From bdcca46bec15ad6705bf65ecca878348aa8b4a35 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 26 Dec 2023 16:04:13 -0500 Subject: [PATCH 1/4] Clean up --- CONTRIBUTING.md | 26 ++--- README.md | 2 +- x/connectivity/connectivity.go | 109 ++++++++++-------- x/connectivity/connectivity_test.go | 86 +++++++------- x/examples/outline-connectivity-app/README.md | 2 +- .../README.md | 4 +- .../main.go | 90 ++++++++------- x/report/report.go | 3 +- 8 files changed, 169 insertions(+), 153 deletions(-) rename x/examples/{outline-connectivity => test-connectivity}/README.md (58%) rename x/examples/{outline-connectivity => test-connectivity}/main.go (74%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dffca7e0..488878df 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,23 +43,23 @@ In Go you can compile for other target operating system and architecture by spec MacOS example: ``` -% GOOS=darwin go build -C x -o ./bin/ ./outline-connectivity -% file ./x/bin/outline-connectivity -./x/bin/outline-connectivity: Mach-O 64-bit executable x86_64 +% GOOS=darwin go build -C x -o ./bin/ ./examples/test-connectivity +% file ./x/bin/test-connectivity +./x/bin/test-connectivity: Mach-O 64-bit executable x86_64 ``` Linux example: ``` -% GOOS=linux go build -C x -o ./bin/ ./outline-connectivity -% file ./x/bin/outline-connectivity -./x/bin/outline-connectivity: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=n0WfUGLum4Y6OpYxZYuz/lbtEdv_kvyUCd3V_qOqb/CC_6GAQqdy_ebeYTdn99/Tk_G3WpBWi8vxqmIlIuU, with debug_info, not stripped +% GOOS=linux go build -C x -o ./bin/ ./examples/test-connectivity +% file ./x/bin/test-connectivity +./x/bin/test-connectivity: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=n0WfUGLum4Y6OpYxZYuz/lbtEdv_kvyUCd3V_qOqb/CC_6GAQqdy_ebeYTdn99/Tk_G3WpBWi8vxqmIlIuU, with debug_info, not stripped ``` Windows example: ``` -% GOOS=windows go build -C x -o ./bin/ ./outline-connectivity -% file ./x/bin/outline-connectivity.exe -./x/bin/outline-connectivity.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows +% GOOS=windows go build -C x -o ./bin/ ./examples/test-connectivity +% file ./x/bin/test-connectivity.exe +./x/bin/test-connectivity.exe: PE32+ executable (console) x86-64 (stripped to external PDB), for MS Windows ``` @@ -103,7 +103,7 @@ podman machine stop The easiest way is to run a binary is to use the [`go run` command](https://pkg.go.dev/cmd/go#hdr-Compile_and_run_Go_program) directly with the `-exec` flag and our convenience tool `run_on_podman.sh`: ```sh -GOOS=linux go run -C x -exec "$(pwd)/run_on_podman.sh" ./outline-connectivity +GOOS=linux go run -C x -exec "$(pwd)/run_on_podman.sh" ./examples/test-connectivity ``` It also works with the [`go test` command](https://pkg.go.dev/cmd/go#hdr-Test_packages): @@ -121,8 +121,8 @@ podman run --arch $(uname -m) --rm -it -v "${bin}":/outline/bin gcr.io/distroles You can also use `podman run` directly to run a pre-built binary: ``` -% podman run --rm -it -v ./x/bin:/outline gcr.io/distroless/static-debian11 /outline/outline-connectivity -Usage of /outline/outline-connectivity: +% podman run --rm -it -v ./x/bin:/outline gcr.io/distroless/static-debian11 /outline/test-connectivity +Usage of /outline/test-connectivity: -domain string Domain name to resolve in the test (default "example.com.") -key string @@ -173,7 +173,7 @@ You can pass `wine64` as the `-exec` parameter in the `go` calls. To build: ```sh -GOOS=windows go run -C x -exec "wine64" ./outline-connectivity +GOOS=windows go run -C x -exec "wine64" ./examples/test-connectivity ``` For tests: diff --git a/README.md b/README.md index 1aa6c188..2ca84a1e 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ This launch is currently in Beta. Most of the code is not new. It's the same cod - [x] Connectivity Test mobile app (iOS and Android) using [Capacitor](https://capacitorjs.com/) - For Go apps - [x] Connectivity Test example [Wails](https://wails.io/) graphical app - - [x] Connectivity Test example command-line app ([source](./x/examples/outline-connectivity/)) + - [x] Connectivity Test example command-line app ([source](./x/examples/test-connectivity/)) - [x] Outline Client example command-line app ([source](./x/examples/outline-cli/)) - [x] Page fetch example command-line app ([source](./x/examples/outline-fetch/)) - [x] Local proxy example command-line app ([source](./x/examples/http2transport/)) diff --git a/x/connectivity/connectivity.go b/x/connectivity/connectivity.go index 77f2dba0..038c0e6d 100644 --- a/x/connectivity/connectivity.go +++ b/x/connectivity/connectivity.go @@ -26,9 +26,9 @@ import ( "github.com/miekg/dns" ) -// TestError captures the observed error of the connectivity test. -type TestError struct { - // Which operation in the test that failed: "dial", "write" or "read" +// ConnectivityError captures the observed error of the connectivity test. +type ConnectivityError struct { + // Which operation in the test that failed: "connect", "send" or "receive" Op string // The POSIX error, when available PosixError string @@ -36,26 +36,41 @@ type TestError struct { Err error } -var _ error = (*TestError)(nil) +var _ error = (*ConnectivityError)(nil) -func (err *TestError) Error() string { +func (err *ConnectivityError) Error() string { return fmt.Sprintf("%v: %v", err.Op, err.Err) } -func (err *TestError) Unwrap() error { +func (err *ConnectivityError) Unwrap() error { return err.Err } -// TestResolverStreamConnectivity uses the given [transport.StreamEndpoint] to connect to a DNS resolver and resolve the test domain. -// The context can be used to set a timeout or deadline, or to pass values to the dialer. -func TestResolverStreamConnectivity(ctx context.Context, resolver transport.StreamEndpoint, testDomain string) (time.Duration, error) { - return testResolver(ctx, resolver.Connect, testDomain) +// Resolver encapsulates the DNS resolution logic for connectivity tests. +type Resolver func(context.Context) (net.Conn, error) + +func (r Resolver) connect(ctx context.Context) (*dns.Conn, error) { + conn, err := r(ctx) + if err != nil { + return nil, err + } + return &dns.Conn{Conn: conn}, nil } -// TestResolverPacketConnectivity uses the given [transport.PacketEndpoint] to connect to a DNS resolver and resolve the test domain. -// The context can be used to set a timeout or deadline, or to pass values to the listener. -func TestResolverPacketConnectivity(ctx context.Context, resolver transport.PacketEndpoint, testDomain string) (time.Duration, error) { - return testResolver(ctx, resolver.Connect, testDomain) +// NewTCPResolver creates a [Resolver] to test StreamDialers. +func NewTCPResolver(dialer transport.StreamDialer, resolverAddr string) Resolver { + endpoint := transport.StreamDialerEndpoint{Dialer: dialer, Address: resolverAddr} + return Resolver(func(ctx context.Context) (net.Conn, error) { + return endpoint.Connect(ctx) + }) +} + +// NewUDPResolver creates a [Resolver] to test PacketDialers. +func NewUDPResolver(dialer transport.PacketDialer, resolverAddr string) Resolver { + endpoint := transport.PacketDialerEndpoint{Dialer: dialer, Address: resolverAddr} + return Resolver(func(ctx context.Context) (net.Conn, error) { + return endpoint.Connect(ctx) + }) } func isTimeout(err error) bool { @@ -63,7 +78,7 @@ func isTimeout(err error) bool { return errors.As(err, &timeErr) && timeErr.Timeout() } -func makeTestError(op string, err error) error { +func makeConnectivityError(op string, err error) *ConnectivityError { var code string var errno syscall.Errno if errors.As(err, &errno) { @@ -71,45 +86,43 @@ func makeTestError(op string, err error) error { } else if isTimeout(err) { code = "ETIMEDOUT" } - return &TestError{Op: op, PosixError: code, Err: err} + return &ConnectivityError{Op: op, PosixError: code, Err: err} } -func testResolver[C net.Conn](ctx context.Context, connect func(context.Context) (C, error), testDomain string) (time.Duration, error) { - deadline, ok := ctx.Deadline() - if !ok { +// TestConnectivityWithResolver tests weather we can get a response from the given [Resolver]. It can be used +// to test connectivity of its underlying [transport.StreamDialer] or [transport.PacketDialer]. +// Invalid tests that cannot assert connectivity will return (nil, error). +// Valid tests will return (*ConnectivityError, nil), where *ConnectivityError will be nil if there's connectivity or +// a structure with details of the error found. +func TestConnectivityWithResolver(ctx context.Context, resolver Resolver, testDomain string) (*ConnectivityError, error) { + if _, ok := ctx.Deadline(); !ok { // Default deadline is 5 seconds. - deadline = time.Now().Add(5 * time.Second) + deadline := time.Now().Add(5 * time.Second) var cancel context.CancelFunc ctx, cancel = context.WithDeadline(ctx, deadline) // Releases the timer. defer cancel() } - testTime := time.Now() - testErr := func() error { - conn, dialErr := connect(ctx) - if dialErr != nil { - return makeTestError("dial", dialErr) - } - defer conn.Close() - conn.SetDeadline(deadline) - dnsConn := dns.Conn{Conn: conn} - - var dnsRequest dns.Msg - dnsRequest.SetQuestion(dns.Fqdn(testDomain), dns.TypeA) - writeErr := dnsConn.WriteMsg(&dnsRequest) - if writeErr != nil { - return makeTestError("write", writeErr) - } - - _, readErr := dnsConn.ReadMsg() - if readErr != nil { - // An early close on the connection may cause a "unexpected EOF" error. That's an application-layer error, - // not triggered by a syscall error so we don't capture an error code. - // TODO: figure out how to standardize on those errors. - return makeTestError("read", readErr) - } - return nil - }() - duration := time.Since(testTime) - return duration, testErr + + dnsConn, err := resolver.connect(ctx) + if err != nil { + return makeConnectivityError("connect", err), nil + } + defer dnsConn.Close() + if deadline, ok := ctx.Deadline(); ok { + dnsConn.SetDeadline(deadline) + } + + var dnsRequest dns.Msg + dnsRequest.SetQuestion(dns.Fqdn(testDomain), dns.TypeA) + if err := dnsConn.WriteMsg(&dnsRequest); err != nil { + return makeConnectivityError("send", err), nil + } + if _, err := dnsConn.ReadMsg(); err != nil { + // An early close on the connection may cause a "unexpected EOF" error. That's an application-layer error, + // not triggered by a syscall error so we don't capture an error code. + // TODO: figure out how to standardize on those errors. + return makeConnectivityError("receive", err), nil + } + return nil, nil } diff --git a/x/connectivity/connectivity_test.go b/x/connectivity/connectivity_test.go index 6fca8883..98ace917 100644 --- a/x/connectivity/connectivity_test.go +++ b/x/connectivity/connectivity_test.go @@ -27,17 +27,18 @@ import ( "time" "github.com/Jigsaw-Code/outline-sdk/transport" - "github.com/miekg/dns" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/net/dns/dnsmessage" ) // StreamDialer Tests func TestTestResolverStreamConnectivityOk(t *testing.T) { // TODO(fortuna): Run a local resolver and make test not depend on an external server. - resolver := &transport.TCPEndpoint{Address: "8.8.8.8:53"} - _, err := TestResolverStreamConnectivity(context.Background(), resolver, "example.com") + resolver := NewTCPResolver(&transport.TCPStreamDialer{}, "8.8.8.8:53") + result, err := TestConnectivityWithResolver(context.Background(), resolver, "example.com") require.NoError(t, err) + require.Nil(t, result) } // TODO: Move this to the SDK. @@ -69,15 +70,15 @@ func TestTestResolverStreamConnectivityRefused(t *testing.T) { // Close right away to ensure the port is closed. The OS will likely not reuse it soon enough. require.Nil(t, listener.Close()) - resolver := &transport.TCPEndpoint{Address: listener.Addr().String()} - _, err = TestResolverStreamConnectivity(context.Background(), resolver, "anything") - var testErr *TestError - require.ErrorAs(t, err, &testErr) - require.Equal(t, "dial", testErr.Op) - require.Equal(t, "ECONNREFUSED", testErr.PosixError) + resolver := NewTCPResolver(&transport.TCPStreamDialer{}, listener.Addr().String()) + result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything") + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, "connect", result.Op) + require.Equal(t, "ECONNREFUSED", result.PosixError) var sysErr *os.SyscallError - require.ErrorAs(t, err, &sysErr) + require.ErrorAs(t, result.Err, &sysErr) expectedSyscall := "connect" if runtime.GOOS == "windows" { expectedSyscall = "connectex" @@ -85,7 +86,7 @@ func TestTestResolverStreamConnectivityRefused(t *testing.T) { require.Equal(t, expectedSyscall, sysErr.Syscall) var errno syscall.Errno - require.ErrorAs(t, sysErr.Err, &errno) + require.ErrorAs(t, result.Err, &errno) require.Equal(t, "ECONNREFUSED", errnoName(errno)) } @@ -104,16 +105,15 @@ func TestTestResolverStreamConnectivityReset(t *testing.T) { }, &running) defer listener.Close() - resolver := &transport.TCPEndpoint{Address: listener.Addr().String()} - _, err := TestResolverStreamConnectivity(context.Background(), resolver, "anything") - - var testErr *TestError - require.ErrorAs(t, err, &testErr) - require.Equalf(t, "read", testErr.Op, "Wrong test operation. Error: %v", testErr.Err) - require.Equal(t, "ECONNRESET", testErr.PosixError) + resolver := NewTCPResolver(&transport.TCPStreamDialer{}, listener.Addr().String()) + result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything") + require.NoError(t, err) + require.NotNil(t, result) + require.Equalf(t, "receive", result.Op, "Wrong test operation. Error: %v", result.Err) + require.Equal(t, "ECONNRESET", result.PosixError) var sysErr *os.SyscallError - require.ErrorAs(t, err, &sysErr) + require.ErrorAs(t, result.Err, &sysErr) expectedSyscall := "read" if runtime.GOOS == "windows" { expectedSyscall = "wsarecv" @@ -121,7 +121,7 @@ func TestTestResolverStreamConnectivityReset(t *testing.T) { require.Equalf(t, expectedSyscall, sysErr.Syscall, "Wrong system call. Error: %v", sysErr) var errno syscall.Errno - require.ErrorAs(t, err, &errno) + require.ErrorAs(t, result.Err, &errno) require.Equal(t, "ECONNRESET", errnoName(errno)) } @@ -136,17 +136,16 @@ func TestTestStreamDialerEarlyClose(t *testing.T) { }, &running) defer listener.Close() - resolver := &transport.TCPEndpoint{Address: listener.Addr().String()} - _, err := TestResolverStreamConnectivity(context.Background(), resolver, "anything") - - var testErr *TestError - require.ErrorAs(t, err, &testErr) - require.Equalf(t, "read", testErr.Op, "Wrong test operation. Error: %v", testErr.Err) - require.Equal(t, "", testErr.PosixError) - require.Error(t, err, "unexpected EOF") + resolver := NewTCPResolver(&transport.TCPStreamDialer{}, listener.Addr().String()) + result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything") + require.NoError(t, err) + require.NotNil(t, result) + require.Equalf(t, "receive", result.Op, "Wrong test operation. Error: %v", result.Err) + require.Equal(t, "", result.PosixError) + require.ErrorIs(t, result.Err, io.EOF) var sysErr *os.SyscallError - require.False(t, errors.As(err, &sysErr)) + require.False(t, errors.As(result.Err, &sysErr)) } func TestTestResolverStreamConnectivityTimeout(t *testing.T) { @@ -161,16 +160,16 @@ func TestTestResolverStreamConnectivityTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) defer cancel() - resolver := &transport.TCPEndpoint{Address: listener.Addr().String()} - _, err := TestResolverStreamConnectivity(ctx, resolver, "anything") + resolver := NewTCPResolver(&transport.TCPStreamDialer{}, listener.Addr().String()) + result, err := TestConnectivityWithResolver(ctx, resolver, "anything") + require.NoError(t, err) + require.NotNil(t, result) - var testErr *TestError - require.ErrorAs(t, err, &testErr) - assert.Equalf(t, "read", testErr.Op, "Wrong test operation. Error: %v", testErr.Err) + assert.Equalf(t, "receive", result.Op, "Wrong test operation. Error: %v", result.Err) - assert.ErrorContains(t, err, "i/o timeout") - assert.True(t, isTimeout(err)) - assert.Equalf(t, "ETIMEDOUT", testErr.PosixError, "Wrong posix error code. Error: %#v, %v", testErr.Err, testErr.Err.Error()) + assert.ErrorContains(t, result.Err, "i/o timeout") + assert.True(t, isTimeout(result.Err)) + assert.Equalf(t, "ETIMEDOUT", result.PosixError, "Wrong posix error code. Error: %#v, %v", result.Err, result.Err.Error()) timeout.Done() listener.Close() @@ -188,21 +187,22 @@ func TestTestPacketPacketConnectivityOk(t *testing.T) { buf := make([]byte, 512) n, clientAddr, err := server.ReadFrom(buf) require.NoError(t, err) - var request dns.Msg + var request dnsmessage.Message err = request.Unpack(buf[:n]) require.NoError(t, err) - var response dns.Msg - response.SetReply(&request) - responseBytes, err := response.Pack() + request.Response = true + request.RecursionAvailable = true + responseBytes, err := request.AppendPack(buf[0:0]) require.NoError(t, err) _, err = server.WriteTo(responseBytes, clientAddr) require.NoError(t, err) }() - resolver := &transport.UDPEndpoint{Address: server.LocalAddr().String()} - _, err = TestResolverPacketConnectivity(context.Background(), resolver, "example.com") + resolver := NewUDPResolver(&transport.UDPPacketDialer{}, server.LocalAddr().String()) + result, err := TestConnectivityWithResolver(context.Background(), resolver, "anything") require.NoError(t, err) + require.Nil(t, result) } // TODO: Add more tests diff --git a/x/examples/outline-connectivity-app/README.md b/x/examples/outline-connectivity-app/README.md index 560f50a0..b0f4e335 100644 --- a/x/examples/outline-connectivity-app/README.md +++ b/x/examples/outline-connectivity-app/README.md @@ -126,7 +126,7 @@ If you just want to develop ios or android, you can run `yarn watch:ios` or `yar 1. **\[P1\]** read browser language on load, centralize language list, and only localize once 1. **\[P1\]** documentation on how to generate mobile app build credentials 1. **\[P1\]** add individual test result errors to the test result output UI -1. **\[P2\]** use x/config to parse the access key and showcase the different transports (see: https://github.com/Jigsaw-Code/outline-sdk/blob/main/x/examples/outline-connectivity/main.go) +1. **\[P2\]** use x/config to parse the access key and showcase the different transports (see: https://github.com/Jigsaw-Code/outline-sdk/blob/main/x/examples/test-connectivity/main.go) 1. **\[P2\]** generalize request handler via generics/reflection 1. **\[P2\]** create a logo for the app 1. **\[P2\]** android-specific toggle CSS diff --git a/x/examples/outline-connectivity/README.md b/x/examples/test-connectivity/README.md similarity index 58% rename from x/examples/outline-connectivity/README.md rename to x/examples/test-connectivity/README.md index 44e81f51..70b4ac7d 100644 --- a/x/examples/outline-connectivity/README.md +++ b/x/examples/test-connectivity/README.md @@ -1,4 +1,4 @@ -# Outline Connectivity Test +# Connectivity Test This app illustrates the use of the Shadowsocks transport to resolve a domain name over TCP or UDP. @@ -8,6 +8,6 @@ Example: KEY=ss://ENCRYPTION_KEY@HOST:PORT/ COLLECTOR_URL=https://collector.example.com/metrics for PREFIX in POST%20 HTTP%2F1.1%20 %05%C3%9C_%C3%A0%01%20 %16%03%01%40%00%01 %13%03%03%3F %16%03%03%40%00%02; do - go run github.com/Jigsaw-Code/outline-sdk/x/examples/outline-connectivity@latest -transport="$KEY?prefix=$PREFIX" -proto tcp -resolver 8.8.8.8 -report-to $COLLECTOR_URL -report-success-rate 0.2 -report-failure-rate 1.0 && echo Prefix "$PREFIX" works! + go run github.com/Jigsaw-Code/outline-sdk/x/examples/test-connectivity@latest -transport="$KEY?prefix=$PREFIX" -proto tcp -resolver 8.8.8.8 -report-to $COLLECTOR_URL -report-success-rate 0.2 -report-failure-rate 1.0 && echo Prefix "$PREFIX" works! done ``` \ No newline at end of file diff --git a/x/examples/outline-connectivity/main.go b/x/examples/test-connectivity/main.go similarity index 74% rename from x/examples/outline-connectivity/main.go rename to x/examples/test-connectivity/main.go index 8489e89b..40bd1a0a 100644 --- a/x/examples/outline-connectivity/main.go +++ b/x/examples/test-connectivity/main.go @@ -30,7 +30,6 @@ import ( "strings" "time" - "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" @@ -62,19 +61,14 @@ type errorJSON struct { Msg string `json:"msg,omitempty"` } -func makeErrorRecord(err error) *errorJSON { - if err == nil { +func makeErrorRecord(result *connectivity.ConnectivityError) *errorJSON { + if result == nil { return nil } var record = new(errorJSON) - var testErr *connectivity.TestError - if errors.As(err, &testErr) { - record.Op = testErr.Op - record.PosixError = testErr.PosixError - record.Msg = unwrapAll(testErr).Error() - } else { - record.Msg = err.Error() - } + record.Op = result.Op + record.PosixError = result.PosixError + record.Msg = unwrapAll(result.Err).Error() return record } @@ -132,6 +126,30 @@ func main() { debugLog = *log.New(os.Stderr, "[DEBUG] ", log.LstdFlags|log.Lmicroseconds|log.Lshortfile) } + var reportCollector report.Collector + if *reportToFlag != "" { + collectorURL, err := url.Parse(*reportToFlag) + if err != nil { + debugLog.Printf("Failed to parse collector URL: %v", err) + } + remoteCollector := &report.RemoteCollector{ + CollectorURL: collectorURL, + HttpClient: &http.Client{Timeout: 10 * time.Second}, + } + retryCollector := &report.RetryCollector{ + Collector: remoteCollector, + MaxRetry: 3, + InitialDelay: 1 * time.Second, + } + reportCollector = &report.SamplingCollector{ + Collector: retryCollector, + SuccessFraction: *reportSuccessFlag, + FailureFraction: *reportFailureFlag, + } + } else { + reportCollector = &report.WriteCollector{Writer: os.Stdout} + } + // Things to test: // - TCP working. Where's the error? // - UDP working @@ -147,62 +165,48 @@ func main() { resolverAddress := net.JoinHostPort(resolverHost, "53") for _, proto := range strings.Split(*protoFlag, ",") { proto = strings.TrimSpace(proto) - testTime := time.Now() - var testErr error - var testDuration time.Duration + var resolver connectivity.Resolver switch proto { case "tcp": streamDialer, err := config.NewStreamDialer(*transportFlag) if err != nil { log.Fatalf("Failed to create StreamDialer: %v", err) } - resolver := &transport.StreamDialerEndpoint{Dialer: streamDialer, Address: resolverAddress} - testDuration, testErr = connectivity.TestResolverStreamConnectivity(context.Background(), resolver, *domainFlag) + resolver = connectivity.NewTCPResolver(streamDialer, resolverAddress) case "udp": packetDialer, err := config.NewPacketDialer(*transportFlag) if err != nil { log.Fatalf("Failed to create PacketDialer: %v", err) } - resolver := &transport.PacketDialerEndpoint{Dialer: packetDialer, Address: resolverAddress} - testDuration, testErr = connectivity.TestResolverPacketConnectivity(context.Background(), resolver, *domainFlag) + resolver = connectivity.NewUDPResolver(packetDialer, resolverAddress) default: log.Fatalf(`Invalid proto %v. Must be "tcp" or "udp"`, proto) } - debugLog.Printf("Test error: %v", testErr) - if testErr == nil { + startTime := time.Now() + result, err := connectivity.TestConnectivityWithResolver(context.Background(), resolver, *domainFlag) + if err != nil { + log.Fatalf("Connectivity test failed to run: %v", err) + } + testDuration := time.Since(startTime) + if result == nil { success = true } + debugLog.Printf("Test %v %v result: %v", proto, resolverAddress, result) var r report.Report = connectivityReport{ Resolver: resolverAddress, Proto: proto, - Time: testTime.UTC().Truncate(time.Second), + Time: startTime.UTC().Truncate(time.Second), // TODO(fortuna): Add tracing to get more detailed info: // Proxy: proxyAddress, // Prefix: config.Prefix.String(), DurationMs: testDuration.Milliseconds(), - Error: makeErrorRecord(testErr), - } - collectorURL, err := url.Parse(*reportToFlag) - if err != nil { - debugLog.Printf("Failed to parse collector URL: %v", err) - } - remoteCollector := &report.RemoteCollector{ - CollectorURL: collectorURL, - HttpClient: &http.Client{Timeout: 10 * time.Second}, - } - retryCollector := &report.RetryCollector{ - Collector: remoteCollector, - MaxRetry: 3, - InitialDelay: 1 * time.Second, + Error: makeErrorRecord(result), } - c := report.SamplingCollector{ - Collector: retryCollector, - SuccessFraction: *reportSuccessFlag, - FailureFraction: *reportFailureFlag, - } - err = c.Collect(context.Background(), r) - if err != nil { - debugLog.Printf("Failed to collect report: %v\n", err) + if reportCollector != nil { + err = reportCollector.Collect(context.Background(), r) + if err != nil { + debugLog.Printf("Failed to collect report: %v\n", err) + } } } if !success { diff --git a/x/report/report.go b/x/report/report.go index fad7661d..cd85fd8a 100644 --- a/x/report/report.go +++ b/x/report/report.go @@ -198,10 +198,9 @@ func (c *WriteCollector) Collect(ctx context.Context, report Report) error { if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } - _, err = c.Writer.Write(jsonData) + _, err = fmt.Fprintln(c.Writer, string(jsonData)) if err != nil { return err } - fmt.Println("Report written") return nil } From 53612912aa3e9720b4edc14e9ddb10d41318ed52 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 26 Dec 2023 17:04:20 -0500 Subject: [PATCH 2/4] Fix outline-cli --- x/examples/outline-cli/outline_packet_proxy.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/x/examples/outline-cli/outline_packet_proxy.go b/x/examples/outline-cli/outline_packet_proxy.go index 3fdfe672..17656380 100644 --- a/x/examples/outline-cli/outline_packet_proxy.go +++ b/x/examples/outline-cli/outline_packet_proxy.go @@ -51,13 +51,17 @@ func newOutlinePacketProxy(transportConfig string) (opp *outlinePacketProxy, err return } -func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolver, domain string) error { +func (proxy *outlinePacketProxy) testConnectivityAndRefresh(resolverAddr, domain string) error { dialer := transport.PacketListenerDialer{Listener: proxy.remotePl} - dnsResolver := &transport.PacketDialerEndpoint{Dialer: dialer, Address: resolver} - _, err := connectivity.TestResolverPacketConnectivity(context.Background(), dnsResolver, domain) + dnsResolver := connectivity.NewUDPResolver(dialer, resolverAddr) + result, err := connectivity.TestConnectivityWithResolver(context.Background(), dnsResolver, domain) if err != nil { - logging.Info.Println("remote server cannot handle UDP traffic, switch to DNS truncate mode") + logging.Info.Printf("connectivity test failed. Refresh skipped. Error: %v\n", err) + return err + } + if result != nil { + logging.Info.Println("remote server cannot handle UDP traffic, switch to DNS truncate mode.") return proxy.SetProxy(proxy.fallback) } else { logging.Info.Println("remote server supports UDP, we will delegate all UDP packets to it") From f4d461de7fd50a0e068040aa4e08d8403ccc6c53 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 26 Dec 2023 17:15:21 -0500 Subject: [PATCH 3/4] Add transport comment. --- x/examples/test-connectivity/main.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/x/examples/test-connectivity/main.go b/x/examples/test-connectivity/main.go index 40bd1a0a..d807d3ea 100644 --- a/x/examples/test-connectivity/main.go +++ b/x/examples/test-connectivity/main.go @@ -43,9 +43,9 @@ type connectivityReport struct { // Inputs Resolver string `json:"resolver"` Proto string `json:"proto"` - // TODO(fortuna): get details from trace - // Proxy string `json:"proxy"` - // Prefix string `json:"prefix"` + // TODO(fortuna): add sanitized transport config. + // Transport string `json:"transport"` + // Observations Time time.Time `json:"time"` DurationMs int64 `json:"duration_ms"` @@ -196,9 +196,8 @@ func main() { Resolver: resolverAddress, Proto: proto, Time: startTime.UTC().Truncate(time.Second), - // TODO(fortuna): Add tracing to get more detailed info: - // Proxy: proxyAddress, - // Prefix: config.Prefix.String(), + // TODO(fortuna): Add sanitized config: + // Transport: config.SanitizedConfig(*transportFlag), DurationMs: testDuration.Milliseconds(), Error: makeErrorRecord(result), } From 476ecfe0af190e3da007818838626c15930b0d79 Mon Sep 17 00:00:00 2001 From: Vinicius Fortuna Date: Tue, 26 Dec 2023 17:20:59 -0500 Subject: [PATCH 4/4] Cleaner deadline --- x/connectivity/connectivity.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/x/connectivity/connectivity.go b/x/connectivity/connectivity.go index 038c0e6d..194beab2 100644 --- a/x/connectivity/connectivity.go +++ b/x/connectivity/connectivity.go @@ -95,9 +95,10 @@ func makeConnectivityError(op string, err error) *ConnectivityError { // Valid tests will return (*ConnectivityError, nil), where *ConnectivityError will be nil if there's connectivity or // a structure with details of the error found. func TestConnectivityWithResolver(ctx context.Context, resolver Resolver, testDomain string) (*ConnectivityError, error) { - if _, ok := ctx.Deadline(); !ok { + deadline, ok := ctx.Deadline() + if !ok { // Default deadline is 5 seconds. - deadline := time.Now().Add(5 * time.Second) + deadline = time.Now().Add(5 * time.Second) var cancel context.CancelFunc ctx, cancel = context.WithDeadline(ctx, deadline) // Releases the timer. @@ -109,9 +110,7 @@ func TestConnectivityWithResolver(ctx context.Context, resolver Resolver, testDo return makeConnectivityError("connect", err), nil } defer dnsConn.Close() - if deadline, ok := ctx.Deadline(); ok { - dnsConn.SetDeadline(deadline) - } + dnsConn.SetDeadline(deadline) var dnsRequest dns.Msg dnsRequest.SetQuestion(dns.Fqdn(testDomain), dns.TypeA)