diff --git a/x/examples/test-connectivity/main.go b/x/examples/test-connectivity/main.go index 45b4ea8d..bc1af5b3 100644 --- a/x/examples/test-connectivity/main.go +++ b/x/examples/test-connectivity/main.go @@ -24,6 +24,7 @@ import ( "log" "net" "net/http" + "net/http/httptrace" "net/url" "os" "path" @@ -31,6 +32,7 @@ import ( "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" @@ -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"` @@ -53,6 +61,23 @@ 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"` + Time time.Time `json:"time"` + DurationMs int64 `json:"duration_ms"` +} + type errorJSON struct { // TODO: add Shadowsocks/Transport error Op string `json:"op,omitempty"` @@ -84,7 +109,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 @@ -98,6 +123,55 @@ func init() { } } +func getReportFromTrace(ctx context.Context, r *connectivityReport, hostname string) context.Context { + var dnsStart, connectStart 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. + r.DNSQueries = append(r.DNSQueries, report) + }, + ConnectStart: func(network, addr string) { + connectStart = time.Now() + }, + ConnectDone: func(network, addr string, connErr error) { + ip, port, err := net.SplitHostPort(addr) + if err != nil { + return + } + if network == "tcp" { + report := tcpReport{ + Hostname: hostname, + IP: ip, + Port: port, + Time: connectStart.UTC().Truncate(time.Second), + DurationMs: time.Since(connectStart).Milliseconds(), + } + if connErr != nil { + report.Error = connErr.Error() + } + // TODO(fortuna): Use a Mutex. + r.TCPConnections = append(r.TCPConnections, report) + } + }, + }) + + return ctx +} + func main() { verboseFlag := flag.Bool("v", false, "Enable debug output") transportFlag := flag.String("transport", "", "Transport config") @@ -161,21 +235,44 @@ 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, ",") { + r := &connectivityReport{ + Test: testReport{}, + DNSQueries: []dnsReport{}, + TCPConnections: []tcpReport{}, + } proto = strings.TrimSpace(proto) var resolver dns.Resolver switch proto { case "tcp": + configToDialer := config.NewDefaultConfigToDialer() + configToDialer.BaseStreamDialer = transport.FuncStreamDialer(func(ctx context.Context, addr string) (transport.StreamConn, error) { + hostname, _, err := net.SplitHostPort(addr) + if err != nil { + return nil, err + } + ctx = getReportFromTrace(ctx, r, hostname) + return (&transport.TCPDialer{}).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 + } + ctx = getReportFromTrace(ctx, r, hostname) + return (&transport.UDPDialer{}).DialPacket(ctx, addr) + }) packetDialer, err := configToDialer.NewPacketDialer(*transportFlag) if err != nil { log.Fatalf("Failed to create PacketDialer: %v", err) @@ -198,7 +295,8 @@ func main() { if err != nil { log.Fatalf("Failed to sanitize config: %v", err) } - var r report.Report = connectivityReport{ + + r.Test = testReport{ Resolver: resolverAddress, Proto: proto, Time: startTime.UTC().Truncate(time.Second), @@ -207,6 +305,7 @@ func main() { DurationMs: testDuration.Milliseconds(), Error: makeErrorRecord(result), } + if reportCollector != nil { err = reportCollector.Collect(context.Background(), r) if err != nil { diff --git a/x/go.sum b/x/go.sum index 7c6f5f80..c69a301f 100644 --- a/x/go.sum +++ b/x/go.sum @@ -40,6 +40,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/cognusion/go-cache-lru v0.0.0-20170419142635-f73e2280ecea h1:9C2rdYRp8Vzwhm3sbFX0yYfB+70zKFRjn7cnPCucHSw= github.com/cognusion/go-cache-lru v0.0.0-20170419142635-f73e2280ecea/go.mod h1:MdyNkAe06D7xmJsf+MsLvbZKYNXuOHLKJrvw+x4LlcQ= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75 h1:1t6cnyP4liW9hjmnVrDn6Bc5m2qlAcjPurf9UEye4Hg= +github.com/Jigsaw-Code/outline-sdk v0.0.17-0.20240708052545-4f51b366fe75/go.mod h1:e1oQZbSdLJBBuHgfeQsgEkvkuyIePPwstUeZRGq0KO8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=