Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add DNS and TCP reports to test-connectivity #272

Merged
merged 9 commits into from
Sep 20, 2024
Merged
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 132 additions & 9 deletions x/examples/test-connectivity/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import (
"log"
"net"
"net/http"
"net/http/httptrace"
"net/url"
"os"
"path"
"strings"
"time"

"github.com/Jigsaw-Code/outline-sdk/dns"
"github.com/Jigsaw-Code/outline-sdk/transport"
"github.com/Jigsaw-Code/outline-sdk/x/config"
"github.com/Jigsaw-Code/outline-sdk/x/connectivity"
"github.com/Jigsaw-Code/outline-sdk/x/report"
Expand All @@ -41,6 +43,12 @@ var debugLog log.Logger = *log.New(io.Discard, "", 0)
// var errorLog log.Logger = *log.New(os.Stderr, "[ERROR] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile)

type connectivityReport struct {
Test testReport `json:"test"`
DNSQueries []dnsReport `json:"dns_queries,omitempty"`
TCPConnections []tcpReport `json:"tcp_connections,omitempty"`
}

type testReport struct {
// Inputs
Resolver string `json:"resolver"`
Proto string `json:"proto"`
Expand All @@ -53,6 +61,21 @@ type connectivityReport struct {
Error *errorJSON `json:"error"`
}

type dnsReport struct {
QueryName string `json:"query_name"`
Time time.Time `json:"time"`
DurationMs int64 `json:"duration_ms"`
AnswerIPs []string `json:"answer_ips"`
Error string `json:"error"`
}

type tcpReport struct {
Hostname string `json:"hostname"`
IP string `json:"ip"`
Port string `json:"port"`
Error string `json:"error"`
}

type errorJSON struct {
// TODO: add Shadowsocks/Transport error
Op string `json:"op,omitempty"`
Expand Down Expand Up @@ -84,7 +107,7 @@ func unwrapAll(err error) error {
}

func (r connectivityReport) IsSuccess() bool {
if r.Error == nil {
if r.Test.Error == nil {
return true
} else {
return false
Expand All @@ -97,6 +120,29 @@ func init() {
flag.PrintDefaults()
}
}
func newTCPTraceDialer(
onDNS func(ctx context.Context, domain string) func(di httptrace.DNSDoneInfo),
onDial func(ctx context.Context, network, addr string, connErr error)) transport.StreamDialer {
dialer := &transport.TCPDialer{}
var onDNSDone func(di httptrace.DNSDoneInfo)
return transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) {
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
DNSStart: func(di httptrace.DNSStartInfo) {
onDNSDone = onDNS(ctx, di.Host)
},
DNSDone: func(di httptrace.DNSDoneInfo) {
if onDNSDone != nil {
onDNSDone(di)
onDNSDone = nil
}
},
ConnectDone: func(network, addr string, connErr error) {
onDial(ctx, network, addr, connErr)
},
})
return dialer.DialStream(ctx, addr)
})
}

func main() {
verboseFlag := flag.Bool("v", false, "Enable debug output")
Expand Down Expand Up @@ -161,21 +207,94 @@ func main() {
success := false
jsonEncoder := json.NewEncoder(os.Stdout)
jsonEncoder.SetEscapeHTML(false)
configToDialer := config.NewDefaultConfigToDialer()
for _, resolverHost := range strings.Split(*resolverFlag, ",") {
resolverHost := strings.TrimSpace(resolverHost)
resolverAddress := net.JoinHostPort(resolverHost, "53")
for _, proto := range strings.Split(*protoFlag, ",") {
proto = strings.TrimSpace(proto)
var resolver dns.Resolver
dnsReports := make([]dnsReport, 0)
tcpReports := make([]tcpReport, 0)
switch proto {
case "tcp":
configToDialer := config.NewDefaultConfigToDialer()
configToDialer.BaseStreamDialer = transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fortuna I believe we should move setting both udp and tcp base dialers outside of the switch statement. In other words, we should set both udp and tcp base dialer regardless of the selected protocol since for example doing udp resolution over socks5 will do both udp and tcp operations.

In the current implementation, we cannot collect dns report if protocol is set to udp (-proto udp) and if the transport is socks5 since it would use plain tcp dialer which does not have the tracer setup.

You can checkout the changes I made on this branch: https://github.com/Jigsaw-Code/outline-sdk/blob/amir-tc-3/x/examples/test-connectivity/main.go

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you try the cli with -proto udp over socks5 transport you can see that dns and tcp connect report are empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in acdc622

hostname, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
onDNS := func(ctx context.Context, domain string) func(di httptrace.DNSDoneInfo) {
dnsStart := time.Now()
return func(di httptrace.DNSDoneInfo) {
report := dnsReport{
QueryName: hostname,
Time: dnsStart.UTC().Truncate(time.Second),
DurationMs: time.Since(dnsStart).Milliseconds(),
}
if di.Err != nil {
report.Error = di.Err.Error()
}
for _, ip := range di.Addrs {
report.AnswerIPs = append(report.AnswerIPs, ip.IP.String())
}
// TODO(fortuna): Use a Mutex.
dnsReports = append(dnsReports, report)
}
}
onDial := func(ctx context.Context, network, addr string, connErr error) {
ip, port, err := net.SplitHostPort(addr)
if err != nil {
return
}
report := tcpReport{
Hostname: hostname,
IP: ip,
Port: port,
}
if connErr != nil {
report.Error = connErr.Error()
}
// TODO(fortuna): Use a Mutex.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// TODO(fortuna): Use a Mutex.
mu.Lock()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in acdc622

tcpReports = append(tcpReports, report)
}
return newTCPTraceDialer(onDNS, onDial).DialStream(ctx, addr)
})
streamDialer, err := configToDialer.NewStreamDialer(*transportFlag)
if err != nil {
log.Fatalf("Failed to create StreamDialer: %v", err)
}
resolver = dns.NewTCPResolver(streamDialer, resolverAddress)

case "udp":
configToDialer := config.NewDefaultConfigToDialer()
configToDialer.BasePacketDialer = transport.FuncPacketDialer(func(ctx context.Context, addr string) (net.Conn, error) {
hostname, _, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
var dnsStart time.Time
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
DNSStart: func(di httptrace.DNSStartInfo) {
dnsStart = time.Now()
},
DNSDone: func(di httptrace.DNSDoneInfo) {
report := dnsReport{
QueryName: hostname,
Time: dnsStart.UTC().Truncate(time.Second),
DurationMs: time.Since(dnsStart).Milliseconds(),
}
if di.Err != nil {
report.Error = di.Err.Error()
}
for _, ip := range di.Addrs {
report.AnswerIPs = append(report.AnswerIPs, ip.IP.String())
}
// TODO(fortuna): Use a Mutex.
dnsReports = append(dnsReports, report)
},
})
return (&transport.UDPDialer{}).DialPacket(ctx, addr)
})
packetDialer, err := configToDialer.NewPacketDialer(*transportFlag)
if err != nil {
log.Fatalf("Failed to create PacketDialer: %v", err)
Expand All @@ -199,13 +318,17 @@ func main() {
log.Fatalf("Failed to sanitize config: %v", err)
}
var r report.Report = connectivityReport{
Resolver: resolverAddress,
Proto: proto,
Time: startTime.UTC().Truncate(time.Second),
// TODO(fortuna): Add sanitized config:
Transport: sanitizedConfig,
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(result),
Test: testReport{
Resolver: resolverAddress,
Proto: proto,
Time: startTime.UTC().Truncate(time.Second),
// TODO(fortuna): Add sanitized config:
Transport: sanitizedConfig,
DurationMs: testDuration.Milliseconds(),
Error: makeErrorRecord(result),
},
DNSQueries: dnsReports,
TCPConnections: tcpReports,
}
if reportCollector != nil {
err = reportCollector.Collect(context.Background(), r)
Expand Down
Loading