Skip to content
Merged
Changes from 1 commit
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
110 changes: 78 additions & 32 deletions x/smart/integrationtest/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,84 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build nettest
// +build nettest

package integrationtest

import (
"bytes"
"context"
"errors"
"fmt"
"log"
"net"
"net/http/httptest"
"runtime"
"strings"
"testing"
"time"

"golang.getoutline.org/sdk/x/configurl"
"golang.getoutline.org/sdk/transport"
"golang.getoutline.org/sdk/x/smart"
"github.com/stretchr/testify/require"
)

// mockStreamDialer intercepts stream connections made by StrategyFinder.
// It redirects connection attempts for specific badssl.com endpoints to local mock servers,
// and fails all other connections to simulate a completely broken network environment.
type mockStreamDialer struct {
base transport.StreamDialer
untrustedHTTPSServerAddr string // Address of the local mock HTTPS server representing mitm-software.badssl.com
captivePortalServerAddr string // Address of the local mock TCP server representing captive-portal.badssl.com:443
}

func (d *mockStreamDialer) DialStream(ctx context.Context, addr string) (transport.StreamConn, error) {
if addr == "mitm-software.badssl.com:443" {
return d.base.DialStream(ctx, d.untrustedHTTPSServerAddr)
}
if addr == "captive-portal.badssl.com:443" {
return d.base.DialStream(ctx, d.captivePortalServerAddr)
}
return nil, errors.New("dial DNS resolver failed")
}
Comment thread
fortuna marked this conversation as resolved.

// errorPacketDialer simulates UDP dial failures by immediately returning an error.
// This mocks broken DNS-over-UDP resolvers (like china.cn, ns1.tic.ir, tmcell.tm).
type errorPacketDialer struct{}

func (d *errorPacketDialer) DialPacket(ctx context.Context, addr string) (net.Conn, error) {
return nil, errors.New("dial DNS resolver failed")
}

// startMockCaptivePortalTCPServer starts a local TCP server that accepts and immediately closes connections.
// This is used to mock a captive portal that interrupts TLS handshakes.
func startMockCaptivePortalTCPServer(t *testing.T) string {
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
return
}
conn.Close()
}
}()
t.Cleanup(func() {
listener.Close()
})
return listener.Addr().String()
}

func Test_Integration_NewDialer_BrokenConfig(t *testing.T) {
if runtime.GOOS == "android" {
// See https://golang.getoutline.org/sdk/issues/504
t.Skip("Skip Smart Dialer integration test on Android until storage is made compatible with android emulator testing")
}

ts := httptest.NewTLSServer(nil)
t.Cleanup(ts.Close)

captivePortalAddr := startMockCaptivePortalTCPServer(t)

configBytes := []byte(`
dns:
# We get censored DNS responses when we send queries to an IP in China.
Expand All @@ -61,57 +114,50 @@ tls:
fallback:
# Nonexistent Outline Server
- ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTprSzdEdHQ0MkJLOE9hRjBKYjdpWGFK@1.2.3.4:9999/?outline=1
# TODO(#513): Psiphon disabled: psiphon-tls is incompatible with Go 1.25.
# - psiphon: {
# "PropagationChannelId":"ID1",
# "SponsorId":"ID2",
# "DisableLocalSocksProxy" : true,
# "DisableLocalHTTPProxy" : true,
# "EstablishTunnelTimeoutSeconds": 1,
# }
- error: {
"error": "failed to start dialer"
}
# Nonexistant local socks5 proxy
- socks5://192.168.1.10:1080
`)

testDomains := []string{"www.example.com", "example.com"}
transportType := ""

logBuffer := new(bytes.Buffer)
logger := log.New(logBuffer, "", log.LstdFlags)

providers := configurl.NewDefaultProviders()
packetDialer, err := providers.NewPacketDialer(context.Background(), transportType)
if err != nil {
require.NoError(t, err)
}
streamDialer, err := providers.NewStreamDialer(context.Background(), transportType)
if err != nil {
require.NoError(t, err)
}

finder := smart.StrategyFinder{
LogWriter: logger.Writer(),
TestTimeout: 5 * time.Second,
StreamDialer: streamDialer,
PacketDialer: packetDialer,
StreamDialer: &mockStreamDialer{
base: &transport.TCPDialer{},
untrustedHTTPSServerAddr: ts.Listener.Addr().String(),
captivePortalServerAddr: captivePortalAddr,
},
PacketDialer: &errorPacketDialer{},
}
// TODO(#513): Psiphon disabled: psiphon-tls is incompatible with Go 1.25.
// finder.RegisterFallbackParser("psiphon", psiphon.ParseConfig)
finder.RegisterFallbackParser("error", func(ctx context.Context, config smart.YAMLNode) (transport.StreamDialer, string, error) {
m, ok := config.(map[string]any)
if !ok {
return nil, "", fmt.Errorf("invalid config of type %T", config)
}
errStr, _ := m["error"].(string)
return nil, "signature_placeholder", errors.New(errStr)
})

_, err = finder.NewDialer(context.Background(), testDomains, configBytes)
_, err := finder.NewDialer(context.Background(), testDomains, configBytes)

require.Error(t, err)
require.Contains(t, err.Error(), "could not find a working fallback: all tests failed")

// Check the content of the log writer.
// Different systems have different network error messages,
// so we only check the broad strokes.
// Different systems have different network error messages, so we only check the broad strokes.
expectedLogs := []string{
"request for A query failed: dial DNS resolver failed:",
`request for A query failed: receive DNS message failed: failed to get HTTP response: Post "https://mitm-software.badssl.com:443/dns-query": tls:`,
"🏃 running test: 'ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTprSzdEdHQ0MkJLOE9hRjBKYjdpWGFK@1.2.3.4:9999/?outline=1'",
// TODO(#513): Psiphon disabled: psiphon-tls is incompatible with Go 1.25.
// "❌ Failed to create fallback[1]: [{psiphon: {DisableLocalHTTPProxy: true, DisableLocalSocksProxy: true, Establish…]: failed to start psiphon dialer: clientlib: tunnel establishment timeout",
"❌ Failed to create fallback[1]:",
"failed to start dialer",
"🏃 running test: 'socks5://192.168.1.10:1080' (domain: www.example.com.)",
}
logContent := logBuffer.String()
Expand Down
Loading