package exchanger import ( "context" "crypto/tls" "net" "strings" "testing" "time" "github.com/go-gost/core/chain" xlogger "github.com/go-gost/x/logger" "github.com/miekg/dns" ) func TestNewExchanger_UDP(t *testing.T) { ex, err := NewExchanger("1.1.1.1:53", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.network != "udp" { t.Errorf("expected udp network, got %q", e.network) } if e.addr != "1.1.1.1:53" { t.Errorf("expected addr 1.1.1.1:53, got %q", e.addr) } } func TestNewExchanger_UDPDefaultPort(t *testing.T) { ex, err := NewExchanger("1.1.1.1", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.addr != "1.1.1.1:53" { t.Errorf("expected addr 1.1.1.1:53 (default port), got %q", e.addr) } } func TestNewExchanger_TCP(t *testing.T) { ex, err := NewExchanger("tcp://1.1.1.1:53", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.network != "tcp" { t.Errorf("expected tcp network, got %q", e.network) } } func TestNewExchanger_TLS(t *testing.T) { ex, err := NewExchanger("tls://1.1.1.1:853", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.network != "tcp" { t.Errorf("expected tls→tcp network, got %q", e.network) } if e.options.tlsConfig == nil { t.Error("expected TLS config to be set") } } func TestNewExchanger_DoT(t *testing.T) { ex, err := NewExchanger("dot://1.1.1.1:853", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.network != "tcp" { t.Errorf("expected dot→tcp network, got %q", e.network) } } func TestNewExchanger_HTTPS(t *testing.T) { ex, err := NewExchanger("https://1.0.0.1/dns-query", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.network != "https" { t.Errorf("expected https network, got %q", e.network) } if e.client == nil { t.Error("expected HTTP client to be created for DoH") } } func TestNewExchanger_DefaultTimeout(t *testing.T) { ex, err := NewExchanger("1.1.1.1:53", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.options.timeout != 5*time.Second { t.Errorf("expected default timeout 5s, got %v", e.options.timeout) } } func TestNewExchanger_CustomTimeout(t *testing.T) { ex, err := NewExchanger("1.1.1.1:53", TimeoutOption(10*time.Second), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.options.timeout != 10*time.Second { t.Errorf("expected timeout 10s, got %v", e.options.timeout) } } func TestNewExchanger_DefaultScheme(t *testing.T) { ex, err := NewExchanger("8.8.8.8", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.network != "udp" { t.Errorf("expected default to udp, got %q", e.network) } } func TestNewExchanger_InvalidURL(t *testing.T) { _, err := NewExchanger("://invalid", LoggerOption(xlogger.Nop())) if err == nil { t.Error("expected error for invalid URL") } } func TestNewExchanger_CustomTLSConfig(t *testing.T) { customTLS := &tls.Config{MinVersion: tls.VersionTLS12} ex, err := NewExchanger("tls://1.1.1.1:853", TLSConfigOption(customTLS), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.options.tlsConfig != customTLS { t.Error("expected custom TLS config to be preserved") } } func TestNewExchanger_SchemeNormalization(t *testing.T) { tests := []struct { addr string network string }{ {"udp://1.1.1.1:53", "udp"}, {"tcp://1.1.1.1:53", "tcp"}, {"tls://1.1.1.1:853", "tcp"}, {"dot://1.1.1.1:853", "tcp"}, {"https://1.1.1.1/dns-query", "https"}, {"1.1.1.1:53", "udp"}, // bare addr → udp {"quic://1.1.1.1:53", "udp"}, // unknown → udp } for _, tt := range tests { ex, err := NewExchanger(tt.addr, LoggerOption(xlogger.Nop())) if err != nil { t.Errorf("NewExchanger(%q): %v", tt.addr, err) continue } e := ex.(*exchanger) if e.network != tt.network { t.Errorf("NewExchanger(%q): network=%q, want %q", tt.addr, e.network, tt.network) } } } func TestNewExchanger_String(t *testing.T) { ex, err := NewExchanger("udp://1.1.1.1:53", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } if ex.String() != "udp://1.1.1.1:53" { t.Errorf("String() = %q, want %q", ex.String(), "udp://1.1.1.1:53") } } func TestNewExchanger_System(t *testing.T) { ex, err := NewExchanger("system", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, ok := ex.(*systemExchanger); !ok { t.Fatalf("NewExchanger(system) = %T, want *systemExchanger", ex) } if ex.String() != "system" { t.Errorf("String() = %q, want system", ex.String()) } } func TestNewExchanger_AutoSystem(t *testing.T) { ex, err := NewExchanger("auto", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, ok := ex.(*systemExchanger); !ok { t.Fatalf("NewExchanger(auto) = %T, want *systemExchanger", ex) } } func TestSystemExchanger_ExchangeA(t *testing.T) { ex := &systemExchanger{ rawAddr: "system", lookupIP: func(_ context.Context, network, host string) ([]net.IP, error) { if network != "ip4" { t.Errorf("network = %q, want ip4", network) } if host != "example.com" { t.Errorf("host = %q, want example.com", host) } return []net.IP{ net.ParseIP("192.0.2.1"), net.ParseIP("2001:db8::1"), }, nil }, } mq := new(dns.Msg) mq.SetQuestion(dns.Fqdn("example.com"), dns.TypeA) packed, err := mq.Pack() if err != nil { t.Fatal(err) } reply, err := ex.Exchange(context.Background(), packed) if err != nil { t.Fatalf("Exchange: %v", err) } mr := new(dns.Msg) if err := mr.Unpack(reply); err != nil { t.Fatalf("Unpack: %v", err) } if len(mr.Answer) != 1 { t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer)) } a, ok := mr.Answer[0].(*dns.A) if !ok { t.Fatalf("Answer[0] = %T, want *dns.A", mr.Answer[0]) } if !a.A.Equal(net.ParseIP("192.0.2.1")) { t.Errorf("A = %s, want 192.0.2.1", a.A) } } func TestSystemExchanger_ExchangeAAAA(t *testing.T) { ex := &systemExchanger{ rawAddr: "system", lookupIP: func(_ context.Context, network, host string) ([]net.IP, error) { if network != "ip6" { t.Errorf("network = %q, want ip6", network) } if host != "example.com" { t.Errorf("host = %q, want example.com", host) } return []net.IP{ net.ParseIP("192.0.2.1"), net.ParseIP("2001:db8::1"), }, nil }, } mq := new(dns.Msg) mq.SetQuestion(dns.Fqdn("example.com"), dns.TypeAAAA) packed, err := mq.Pack() if err != nil { t.Fatal(err) } reply, err := ex.Exchange(context.Background(), packed) if err != nil { t.Fatalf("Exchange: %v", err) } mr := new(dns.Msg) if err := mr.Unpack(reply); err != nil { t.Fatalf("Unpack: %v", err) } if len(mr.Answer) != 1 { t.Fatalf("len(Answer) = %d, want 1", len(mr.Answer)) } aaaa, ok := mr.Answer[0].(*dns.AAAA) if !ok { t.Fatalf("Answer[0] = %T, want *dns.AAAA", mr.Answer[0]) } if !aaaa.AAAA.Equal(net.ParseIP("2001:db8::1")) { t.Errorf("AAAA = %s, want 2001:db8::1", aaaa.AAAA) } } func TestExchange_DialFailure(t *testing.T) { ex, err := NewExchanger("udp://127.0.0.1:1", TimeoutOption(100*time.Millisecond), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = ex.Exchange(context.Background(), []byte{0}) if err == nil { t.Error("expected error for failed dial") } } func TestExchange_ContextCancelled(t *testing.T) { ex, err := NewExchanger("udp://1.1.1.1:53", TimeoutOption(5*time.Second), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } ctx, cancel := context.WithCancel(context.Background()) cancel() _, err = ex.Exchange(ctx, []byte{0}) if err == nil { t.Error("expected error for cancelled context") } } func TestExchange_TimeoutExceeded(t *testing.T) { ex, err := NewExchanger("tcp://192.0.2.1:53", TimeoutOption(50*time.Millisecond), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } start := time.Now() _, err = ex.Exchange(context.Background(), []byte{0}) elapsed := time.Since(start) if err == nil { t.Error("expected timeout error") } if elapsed > 2*time.Second { t.Errorf("timeout took too long: %v", elapsed) } } func TestDoHExchange_Failure(t *testing.T) { ex, err := NewExchanger("https://1.0.0.1/dns-query", TimeoutOption(100*time.Millisecond), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } _, err = ex.Exchange(context.Background(), []byte("not-a-dns-query")) if err == nil { t.Error("expected error for DoH exchange failure") } } func TestNewExchanger_BareAddr(t *testing.T) { ex, err := NewExchanger("8.8.8.8", LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if !strings.Contains(e.addr, "53") { t.Errorf("expected default port 53 in addr, got %q", e.addr) } } func TestNewExchanger_WithRouter(t *testing.T) { r := &stubRouter{} ex, err := NewExchanger("udp://1.1.1.1:53", RouterOption(r), LoggerOption(xlogger.Nop())) if err != nil { t.Fatalf("unexpected error: %v", err) } e := ex.(*exchanger) if e.router != r { t.Error("expected custom router to be set") } } // stubRouter implements chain.Router for tests. type stubRouter struct{} func (s *stubRouter) Options() *chain.RouterOptions { return nil } func (s *stubRouter) Dial(_ context.Context, _, _ string, _ ...chain.DialOption) (net.Conn, error) { return nil, net.UnknownNetworkError("stub") } func (s *stubRouter) Bind(_ context.Context, _, _ string, _ ...chain.BindOption) (net.Listener, error) { return nil, net.UnknownNetworkError("stub") }