diff --git a/internal/netxlite/filtering/doc.go b/internal/netxlite/filtering/doc.go index 46bf2f6..65018ab 100644 --- a/internal/netxlite/filtering/doc.go +++ b/internal/netxlite/filtering/doc.go @@ -1,2 +1,13 @@ -// Package filtering contains primitives for implementing filtering. +// Package filtering allows to implement self-censorship. +// +// The top-level struct is the TProxy. It implements netxlite's +// TProxable interface. Therefore, you can use TProxy to +// implement filtering and blocking of TCP, TLS, QUIC, DNS, HTTP. +// +// We also expose proxies that implement filtering policies for +// DNS, TLS, and HTTP. +// +// The typical usage of this package's functionality is to +// load a censoring policy into TProxyConfig and then to create +// and start a TProxy instance using NewTProxy. package filtering diff --git a/internal/netxlite/filtering/logger.go b/internal/netxlite/filtering/logger.go new file mode 100644 index 0000000..65092d1 --- /dev/null +++ b/internal/netxlite/filtering/logger.go @@ -0,0 +1,23 @@ +package filtering + +// Logger defines the common interface that a logger should have. It is +// out of the box compatible with `log.Log` in `apex/log`. +type Logger interface { + // Debug emits a debug message. + Debug(msg string) + + // Debugf formats and emits a debug message. + Debugf(format string, v ...interface{}) + + // Info emits an informational message. + Info(msg string) + + // Infof formats and emits an informational message. + Infof(format string, v ...interface{}) + + // Warn emits a warning message. + Warn(msg string) + + // Warnf formats and emits a warning message. + Warnf(format string, v ...interface{}) +} diff --git a/internal/netxlite/filtering/testdata/invalid.json b/internal/netxlite/filtering/testdata/invalid.json new file mode 100644 index 0000000..98232c6 --- /dev/null +++ b/internal/netxlite/filtering/testdata/invalid.json @@ -0,0 +1 @@ +{ diff --git a/internal/netxlite/filtering/testdata/valid.json b/internal/netxlite/filtering/testdata/valid.json new file mode 100644 index 0000000..9b437e2 --- /dev/null +++ b/internal/netxlite/filtering/testdata/valid.json @@ -0,0 +1,5 @@ +{ + "Domains": { + "x.org": "pass" + } +} diff --git a/internal/netxlite/filtering/tproxy.go b/internal/netxlite/filtering/tproxy.go new file mode 100644 index 0000000..43f200b --- /dev/null +++ b/internal/netxlite/filtering/tproxy.go @@ -0,0 +1,343 @@ +package filtering + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "net" + "os" + "time" + + "github.com/miekg/dns" + "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/netxlite/quicx" +) + +// TProxyPolicy is a policy for TPRoxy. +type TProxyPolicy string + +const ( + // TProxyPolicyTCPDropSYN simulates a SYN segment being dropped. + TProxyPolicyTCPDropSYN = TProxyPolicy("tcp-drop-syn") + + // TProxyPolicyTCPRejectSYN simulates a closed TCP port. + TProxyPolicyTCPRejectSYN = TProxyPolicy("tcp-reject-syn") + + // TProxyPolicyDropData drops outgoing data of an + // established TCP/UDP connection. + TProxyPolicyDropData = TProxyPolicy("drop-data") + + // TProxyPolicyHijackDNS causes the dialer to replace the target + // address with the address of the local censored resolver. + TProxyPolicyHijackDNS = TProxyPolicy("hijack-dns") + + // TProxyPolicyHijackTLS causes the dialer to replace the target + // address with the address of the local censored TLS server. + TProxyPolicyHijackTLS = TProxyPolicy("hijack-tls") + + // TProxyPolicyHijackHTTP causes the dialer to replace the target + // address with the address of the local censored HTTP server. + TProxyPolicyHijackHTTP = TProxyPolicy("hijack-http") +) + +// TProxyConfig contains configuration for TProxy. +type TProxyConfig struct { + // Domains contains rules for filtering the lookup of domains. Note + // that the map MUST contain FQDNs. That is, you need to append + // a final dot to the domain name (e.g., `example.com.`). If you + // use the NewTProxyConfig factory, you don't need to worry about this + // issue, because the factory will canonicalize non-canonical + // entries. Otherwise, you can explicitly call the CanonicalizeDNS + // method _before_ using the TProxy. + Domains map[string]DNSAction + + // Endpoints contains rules for filtering TCP/UDP endpoints. + Endpoints map[string]TProxyPolicy + + // SNIs contains rules for filtering TLS SNIs. + SNIs map[string]TLSAction + + // Hosts contains rules for filtering by HTTP host. + Hosts map[string]HTTPAction +} + +// NewTProxyConfig reads the TProxyConfig from the given file. +func NewTProxyConfig(file string) (*TProxyConfig, error) { + data, err := os.ReadFile(file) + if err != nil { + return nil, err + } + var config TProxyConfig + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + config.CanonicalizeDNS() + return &config, nil +} + +// CanonicalizeDNS ensures all DNS names are canonicalized. This method +// modifies the TProxyConfig structure in place. +func (c *TProxyConfig) CanonicalizeDNS() { + domains := make(map[string]DNSAction) + for domain, policy := range c.Domains { + domains[dns.CanonicalName(domain)] = policy + } + c.Domains = domains +} + +// TProxy is a netxlite.TProxable that implements self censorship. +type TProxy struct { + // config contains settings for TProxy. + config *TProxyConfig + + // dnsClient is the DNS client we'll internally use. + dnsClient netxlite.Resolver + + // dnsListener is the DNS listener. + dnsListener DNSListener + + // httpListener is the HTTP listener. + httpListener net.Listener + + // listenUDP allows overriding net.ListenUDP calls in tests + listenUDP func(network string, laddr *net.UDPAddr) (quicx.UDPLikeConn, error) + + // logger is the underlying logger to use. + logger Logger + + // tlsListener is the TLS listener. + tlsListener net.Listener +} + +// +// Constructor and destructor +// + +// NewTProxy creates a new TProxy instance. +func NewTProxy(config *TProxyConfig, logger Logger) (*TProxy, error) { + return newTProxy(config, logger, "127.0.0.1:0", "127.0.0.1:0", "127.0.0.1:0") +} + +func newTProxy(config *TProxyConfig, logger Logger, dnsListenerAddr, + tlsListenerAddr, httpListenerAddr string) (*TProxy, error) { + p := &TProxy{ + config: config, + listenUDP: func(network string, laddr *net.UDPAddr) (quicx.UDPLikeConn, error) { + return net.ListenUDP(network, laddr) + }, + logger: logger, + } + if err := p.newDNSListener(dnsListenerAddr); err != nil { + return nil, err + } + p.newDNSClient(logger) + if err := p.newTLSListener(tlsListenerAddr, logger); err != nil { + p.dnsListener.Close() + return nil, err + } + if err := p.newHTTPListener(httpListenerAddr); err != nil { + p.dnsListener.Close() + p.tlsListener.Close() + return nil, err + } + return p, nil +} + +func (p *TProxy) newDNSListener(listenAddr string) error { + var err error + dnsProxy := &DNSProxy{OnQuery: p.onQuery} + p.dnsListener, err = dnsProxy.Start(listenAddr) + return err +} + +func (p *TProxy) newDNSClient(logger Logger) { + dialer := netxlite.NewDialerWithoutResolver(logger) + p.dnsClient = netxlite.NewResolverUDP(logger, dialer, p.dnsListener.LocalAddr().String()) +} + +func (p *TProxy) newTLSListener(listenAddr string, logger Logger) error { + var err error + tlsProxy := &TLSProxy{OnIncomingSNI: p.onIncomingSNI} + p.tlsListener, err = tlsProxy.Start(listenAddr) + return err +} + +func (p *TProxy) newHTTPListener(listenAddr string) error { + var err error + httpProxy := &HTTPProxy{OnIncomingHost: p.onIncomingHost} + p.httpListener, err = httpProxy.Start(listenAddr) + return err +} + +// Close closes the resources used by a TProxy. +func (p *TProxy) Close() error { + p.dnsClient.CloseIdleConnections() + p.dnsListener.Close() + p.httpListener.Close() + p.tlsListener.Close() + return nil +} + +// +// QUIC +// + +// ListenUDP implements netxlite.TProxy.ListenUDP. +func (p *TProxy) ListenUDP(network string, laddr *net.UDPAddr) (quicx.UDPLikeConn, error) { + pconn, err := p.listenUDP(network, laddr) + if err != nil { + return nil, err + } + return &tProxyUDPLikeConn{UDPLikeConn: pconn, proxy: p}, nil +} + +// tProxyUDPLikeConn is a TProxy-aware UDPLikeConn. +type tProxyUDPLikeConn struct { + // UDPLikeConn is the underlying conn type. + quicx.UDPLikeConn + + // proxy refers to the TProxy. + proxy *TProxy +} + +// WriteTo implements UDPLikeConn.WriteTo. This function will +// apply the proper tproxy policies, if required. +func (c *tProxyUDPLikeConn) WriteTo(pkt []byte, addr net.Addr) (int, error) { + endpoint := fmt.Sprintf("%s/%s", addr.String(), addr.Network()) + policy := c.proxy.config.Endpoints[endpoint] + switch policy { + case TProxyPolicyDropData: + c.proxy.logger.Infof("tproxy: WriteTo: %s => %s", endpoint, policy) + return len(pkt), nil + default: + return c.UDPLikeConn.WriteTo(pkt, addr) + } +} + +// +// System resolver +// + +// LookupHost implements netxlite.TProxy.LookupHost. +func (p *TProxy) LookupHost(ctx context.Context, domain string) ([]string, error) { + return p.dnsClient.LookupHost(ctx, domain) +} + +// +// Dialer +// + +// NewTProxyDialer implements netxlite.TProxy.NewTProxyDialer. +func (p *TProxy) NewTProxyDialer(timeout time.Duration) netxlite.TProxyDialer { + return &tProxyDialer{ + dialer: &net.Dialer{Timeout: timeout}, + proxy: p, + } +} + +// tProxyDialer is a TProxy-aware Dialer. +type tProxyDialer struct { + // dialer is the underlying network dialer. + dialer *net.Dialer + + // proxy refers to the TProxy. + proxy *TProxy +} + +// DialContext behaves like net.Dialer.DialContext. This function will +// apply the proper tproxy policies, if required. +func (d *tProxyDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) { + endpoint := fmt.Sprintf("%s/%s", address, network) + policy := d.proxy.config.Endpoints[endpoint] + switch policy { + case TProxyPolicyTCPDropSYN: + d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) + var cancel context.CancelFunc + const timeout = 70 * time.Second + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + <-ctx.Done() + return nil, errors.New("i/o timeout") + case TProxyPolicyTCPRejectSYN: + d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) + return nil, netxlite.ECONNREFUSED + case TProxyPolicyHijackDNS: + d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) + address = d.proxy.dnsListener.LocalAddr().String() + case TProxyPolicyHijackTLS: + d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) + address = d.proxy.tlsListener.Addr().String() + case TProxyPolicyHijackHTTP: + d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) + address = d.proxy.httpListener.Addr().String() + default: + // nothing + } + conn, err := d.dialer.DialContext(ctx, network, address) + if err != nil { + return nil, err + } + return &tProxyConn{Conn: conn, proxy: d.proxy}, nil +} + +// tProxyConn is a TProxy-aware net.Conn. +type tProxyConn struct { + // Conn is the underlying conn. + net.Conn + + // proxy refers to the TProxy. + proxy *TProxy +} + +// Write implements Conn.Write. This function will apply +// the proper tproxy policies, if required. +func (c *tProxyConn) Write(b []byte) (int, error) { + addr := c.Conn.RemoteAddr() + endpoint := fmt.Sprintf("%s/%s", addr.String(), addr.Network()) + policy := c.proxy.config.Endpoints[endpoint] + switch policy { + case TProxyPolicyDropData: + c.proxy.logger.Infof("tproxy: Write: %s => %s", endpoint, policy) + return len(b), nil + default: + return c.Conn.Write(b) + } +} + +// +// Filtering policies implementation +// + +// onQuery is called for filtering outgoing DNS queries. +func (p *TProxy) onQuery(domain string) DNSAction { + policy := p.config.Domains[domain] + if policy == "" { + policy = DNSActionPass + } else { + p.logger.Infof("tproxy: DNS: %s => %s", domain, policy) + } + return policy +} + +// onIncomingSNI is called for filtering SNI values. +func (p *TProxy) onIncomingSNI(sni string) TLSAction { + policy := p.config.SNIs[sni] + if policy == "" { + policy = TLSActionPass + } else { + p.logger.Infof("tproxy: TLS: %s => %s", sni, policy) + } + return policy +} + +// onIncomingHost is called for filtering HTTP hosts. +func (p *TProxy) onIncomingHost(host string) HTTPAction { + policy := p.config.Hosts[host] + if policy == "" { + policy = HTTPActionPass + } else { + p.logger.Infof("tproxy: HTTP: %s => %s", host, policy) + } + return policy +} diff --git a/internal/netxlite/filtering/tproxy_test.go b/internal/netxlite/filtering/tproxy_test.go new file mode 100644 index 0000000..038ec33 --- /dev/null +++ b/internal/netxlite/filtering/tproxy_test.go @@ -0,0 +1,521 @@ +package filtering + +import ( + "context" + "crypto/tls" + "errors" + "net" + "net/http" + "path/filepath" + "strings" + "syscall" + "testing" + "time" + + "github.com/apex/log" + "github.com/ooni/probe-cli/v3/internal/netxlite" + "github.com/ooni/probe-cli/v3/internal/netxlite/mocks" + "github.com/ooni/probe-cli/v3/internal/netxlite/quicx" +) + +// tProxyDialerAdapter adapts a netxlite.TProxyDialer to be a netxlite.Dialer. +type tProxyDialerAdapter struct { + netxlite.TProxyDialer +} + +// CloseIdleConnections implements Dialer.CloseIdleConnections. +func (*tProxyDialerAdapter) CloseIdleConnections() { + // nothing +} + +func TestNewTProxyConfig(t *testing.T) { + t.Run("with nonexistent file", func(t *testing.T) { + config, err := NewTProxyConfig(filepath.Join("testdata", "nonexistent")) + if !errors.Is(err, syscall.ENOENT) { + t.Fatal("unexpected err", err) + } + if config != nil { + t.Fatal("expected nil config here") + } + }) + + t.Run("with file containing invalid JSON", func(t *testing.T) { + config, err := NewTProxyConfig(filepath.Join("testdata", "invalid.json")) + if err == nil || !strings.HasSuffix(err.Error(), "unexpected end of JSON input") { + t.Fatal("unexpected err", err) + } + if config != nil { + t.Fatal("expected nil config here") + } + }) + + t.Run("with file containing valid JSON", func(t *testing.T) { + config, err := NewTProxyConfig(filepath.Join("testdata", "valid.json")) + if err != nil { + t.Fatal(err) + } + if config == nil { + t.Fatal("expected non-nil config here") + } + if config.Domains["x.org."] != "pass" { + t.Fatal("did not auto-canonicalize names") + } + }) +} + +func TestNewTProxy(t *testing.T) { + t.Run("successful creation and destruction", func(t *testing.T) { + config := &TProxyConfig{} + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + if err := proxy.Close(); err != nil { + t.Fatal(err) + } + }) + + t.Run("cannot create DNS listener", func(t *testing.T) { + config := &TProxyConfig{} + proxy, err := newTProxy(config, log.Log, "127.0.0.1", "", "") + if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { + t.Fatal("unexpected err", err) + } + if proxy != nil { + t.Fatal("expected nil proxy here") + } + }) + + t.Run("cannot create TLS listener", func(t *testing.T) { + config := &TProxyConfig{} + proxy, err := newTProxy(config, log.Log, "127.0.0.1:0", "127.0.0.1", "") + if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { + t.Fatal("unexpected err", err) + } + if proxy != nil { + t.Fatal("expected nil proxy here") + } + }) + + t.Run("cannot create HTTP listener", func(t *testing.T) { + config := &TProxyConfig{} + proxy, err := newTProxy(config, log.Log, "127.0.0.1:0", "127.0.0.1:0", "127.0.0.1") + if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { + t.Fatal("unexpected err", err) + } + if proxy != nil { + t.Fatal("expected nil proxy here") + } + }) +} + +func TestTProxyQUIC(t *testing.T) { + t.Run("ListenUDP", func(t *testing.T) { + t.Run("failure", func(t *testing.T) { + proxy, err := NewTProxy(&TProxyConfig{}, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + pconn, err := proxy.ListenUDP("tcp", &net.UDPAddr{}) + if err == nil || !strings.HasSuffix(err.Error(), "unknown network tcp") { + t.Fatal("unexpected err", err) + } + if pconn != nil { + t.Fatal("expected nil pconn here") + } + }) + + t.Run("success", func(t *testing.T) { + proxy, err := NewTProxy(&TProxyConfig{}, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{}) + if err != nil { + t.Fatal(err) + } + uconn := pconn.(*tProxyUDPLikeConn) + if uconn.proxy != proxy { + t.Fatal("proxy not correctly set") + } + if _, okay := uconn.UDPLikeConn.(*net.UDPConn); !okay { + t.Fatal("underlying connection should be an UDPConn") + } + uconn.Close() + }) + }) + + t.Run("WriteTo", func(t *testing.T) { + t.Run("without the drop policy", func(t *testing.T) { + proxy, err := NewTProxy(&TProxyConfig{}, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + var called bool + proxy.listenUDP = func(network string, laddr *net.UDPAddr) (quicx.UDPLikeConn, error) { + return &mocks.QUICUDPLikeConn{ + MockWriteTo: func(p []byte, addr net.Addr) (int, error) { + called = true + return len(p), nil + }, + }, nil + } + pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{}) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 128) + count, err := pconn.WriteTo(data, &net.UDPAddr{}) + if err != nil { + t.Fatal(err) + } + if count != len(data) { + t.Fatal("unexpected number of bytes written") + } + if !called { + t.Fatal("not called") + } + }) + + t.Run("with the drop policy", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDropData, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + var called bool + proxy.listenUDP = func(network string, laddr *net.UDPAddr) (quicx.UDPLikeConn, error) { + return &mocks.QUICUDPLikeConn{ + MockWriteTo: func(p []byte, addr net.Addr) (int, error) { + called = true + return len(p), nil + }, + }, nil + } + pconn, err := proxy.ListenUDP("udp", &net.UDPAddr{}) + if err != nil { + t.Fatal(err) + } + data := make([]byte, 128) + destAddr := &net.UDPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 1234, + Zone: "", + } + count, err := pconn.WriteTo(data, destAddr) + if err != nil { + t.Fatal(err) + } + if count != len(data) { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + }) +} + +func TestTProxyLookupHost(t *testing.T) { + t.Run("without filtering", func(t *testing.T) { + config := &TProxyConfig{} + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + ctx := context.Background() + addrs, err := proxy.LookupHost(ctx, "dns.google") + if err != nil { + t.Fatal(err) + } + if len(addrs) < 2 { + t.Fatal("too few addrs") + } + }) + + t.Run("with filtering", func(t *testing.T) { + config := &TProxyConfig{ + Domains: map[string]DNSAction{ + "dns.google.": DNSActionNXDOMAIN, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + ctx := context.Background() + addrs, err := proxy.LookupHost(ctx, "dns.google") + if err == nil || err.Error() != "dns_nxdomain_error" { + t.Fatal("unexpected err", err) + } + if len(addrs) != 0 { + t.Fatal("too many addrs") + } + }) +} + +func TestTProxyOnIncomingSNI(t *testing.T) { + t.Run("without filtering", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:443/tcp": TProxyPolicyHijackTLS, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + ctx := context.Background() + dialer := proxy.NewTProxyDialer(10 * time.Second) + conn, err := dialer.DialContext(ctx, "tcp", "8.8.8.8:443") + if err != nil { + t.Fatal(err) + } + tconn := tls.Client(conn, &tls.Config{ServerName: "dns.google"}) + err = tconn.HandshakeContext(ctx) + if err != nil { + t.Fatal(err) + } + tconn.Close() + }) + + t.Run("with filtering", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:443/tcp": TProxyPolicyHijackTLS, + }, + SNIs: map[string]TLSAction{ + "dns.google": TLSActionReset, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + ctx := context.Background() + dialer := proxy.NewTProxyDialer(10 * time.Second) + conn, err := dialer.DialContext(ctx, "tcp", "8.8.8.8:443") + if err != nil { + t.Fatal(err) + } + tlsh := netxlite.NewTLSHandshakerStdlib(log.Log) + tconn, _, err := tlsh.Handshake(ctx, conn, &tls.Config{ServerName: "dns.google"}) + if err == nil || err.Error() != netxlite.FailureConnectionReset { + t.Fatal("unexpected err", err) + } + if tconn != nil { + t.Fatal("expected nil tconn") + } + conn.Close() + }) +} + +func TestTProxyOnIncomingHost(t *testing.T) { + t.Run("without filtering", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "130.192.16.171:80/tcp": TProxyPolicyHijackHTTP, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := proxy.NewTProxyDialer(10 * time.Second) + req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "nexa.polito.it" + txp := &http.Transport{DialContext: dialer.DialContext} + resp, err := txp.RoundTrip(req) + if err != nil { + t.Fatal(err) + } + resp.Body.Close() + }) + + t.Run("with filtering", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "130.192.16.171:80/tcp": TProxyPolicyHijackHTTP, + }, + Hosts: map[string]HTTPAction{ + "nexa.polito.it": HTTPActionReset, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := netxlite.WrapDialer( + log.Log, + netxlite.NewResolverStdlib(log.Log), + &tProxyDialerAdapter{ + proxy.NewTProxyDialer(10 * time.Second), + }, + ) + req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "nexa.polito.it" + txp := &http.Transport{DialContext: dialer.DialContext} + resp, err := txp.RoundTrip(req) + if err == nil || !strings.HasSuffix(err.Error(), netxlite.FailureConnectionReset) { + t.Fatal("unexpected err", err) + } + if resp != nil { + t.Fatal("expected nil resp here") + } + }) +} + +func TestTProxyDial(t *testing.T) { + t.Run("with drop SYN", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "130.192.16.171:80/tcp": TProxyPolicyTCPDropSYN, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := proxy.NewTProxyDialer(10 * time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + req, err := http.NewRequestWithContext(ctx, "GET", "http://130.192.16.171:80", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "nexa.polito.it" + txp := &http.Transport{DialContext: dialer.DialContext} + resp, err := txp.RoundTrip(req) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatal("unexpected err", err) + } + if resp != nil { + t.Fatal("expected nil resp here") + } + }) + + t.Run("with reject SYN", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "130.192.16.171:80/tcp": TProxyPolicyTCPRejectSYN, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := netxlite.WrapDialer(log.Log, + netxlite.NewResolverStdlib(log.Log), + &tProxyDialerAdapter{ + proxy.NewTProxyDialer(10 * time.Second)}) + req, err := http.NewRequest("GET", "http://130.192.16.171:80", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "nexa.polito.it" + txp := &http.Transport{DialContext: dialer.DialContext} + resp, err := txp.RoundTrip(req) + if err == nil || !strings.HasSuffix(err.Error(), netxlite.FailureConnectionRefused) { + t.Fatal("unexpected err", err) + } + if resp != nil { + t.Fatal("expected nil resp here") + } + }) + + t.Run("with drop data", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "130.192.16.171:80/tcp": TProxyPolicyDropData, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := proxy.NewTProxyDialer(10 * time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + req, err := http.NewRequestWithContext( + ctx, "GET", "http://130.192.16.171:80", nil) + if err != nil { + t.Fatal(err) + } + req.Host = "nexa.polito.it" + txp := &http.Transport{DialContext: dialer.DialContext} + resp, err := txp.RoundTrip(req) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatal("unexpected err", err) + } + if resp != nil { + t.Fatal("expected nil resp here") + } + }) + + t.Run("with hijack DNS", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:53/udp": TProxyPolicyHijackDNS, + }, + Domains: map[string]DNSAction{ + "example.com.": DNSActionNXDOMAIN, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := proxy.NewTProxyDialer(10 * time.Second) + resolver := netxlite.NewResolverUDP( + log.Log, &tProxyDialerAdapter{dialer}, "8.8.8.8:53") + addrs, err := resolver.LookupHost(context.Background(), "example.com") + if err == nil || err.Error() != netxlite.FailureDNSNXDOMAINError { + t.Fatal("unexpected err", err) + } + if len(addrs) != 0 { + t.Fatal("expected no addrs here") + } + }) + + t.Run("with invalid destination address", func(t *testing.T) { + config := &TProxyConfig{} + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + dialer := proxy.NewTProxyDialer(10 * time.Second) + ctx := context.Background() + conn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1") + if err == nil || !strings.HasSuffix(err.Error(), "missing port in address") { + t.Fatal("unexpected err", err) + } + if conn != nil { + t.Fatal("expected nil conn here") + } + }) +}