From 851b9913fae77e6b2d57fd7b1c1c04ee5a187ab1 Mon Sep 17 00:00:00 2001 From: Simone Basso Date: Wed, 3 Nov 2021 00:29:14 +0100 Subject: [PATCH] feat(filtering): implement the divert policy (#569) This is the policy we need to provoke certificate errors. We'll divert from, say, `8.8.8.8:443/udp` to, say, `1.1.1.1:443/udp`. We'll do something similar for `443/tcp`. This will cause certificate validation errors. With this change, we have now implemented the simple design described by https://github.com/ooni/probe/issues/1803#issuecomment-957323297. --- internal/netxlite/filtering/tproxy.go | 78 ++++ internal/netxlite/filtering/tproxy_test.go | 419 +++++++++++++++++++++ 2 files changed, 497 insertions(+) diff --git a/internal/netxlite/filtering/tproxy.go b/internal/netxlite/filtering/tproxy.go index 59fb03f..6ed50f7 100644 --- a/internal/netxlite/filtering/tproxy.go +++ b/internal/netxlite/filtering/tproxy.go @@ -7,6 +7,8 @@ import ( "fmt" "net" "os" + "strconv" + "strings" "time" "github.com/miekg/dns" @@ -39,6 +41,10 @@ const ( // TProxyPolicyHijackHTTP causes the dialer to replace the target // address with the address of the local censored HTTP server. TProxyPolicyHijackHTTP = TProxyPolicy("hijack-http") + + // TProxyPolicyDivert causes the dialer, or WriteTo, to look into the + // divert table to map the endpoint to another endpoint. + TProxyPolicyDivert = TProxyPolicy("divert") ) // TProxyConfig contains configuration for TProxy. @@ -52,6 +58,10 @@ type TProxyConfig struct { // method _before_ using the TProxy. DNSCache map[string][]string + // Divert is a table that maps an endpoint to another endpoint. This + // table is only cheched when using the "divert" policy in the Endpoints table. + Divert map[string]string + // 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 @@ -224,11 +234,53 @@ func (c *tProxyUDPLikeConn) WriteTo(pkt []byte, addr net.Addr) (int, error) { case TProxyPolicyDropData: c.proxy.logger.Infof("tproxy: WriteTo: %s => %s", endpoint, policy) return len(pkt), nil + case TProxyPolicyDivert: + c.proxy.logger.Infof("tproxy: WriteTo: %s => %s", endpoint, policy) + return c.writeToWithDivert(pkt, endpoint) default: return c.UDPLikeConn.WriteTo(pkt, addr) } } +var ( + errMissingDivertEntry = errors.New("tproxy: missing divert entry") + errInvalidDivertProtocol = errors.New("tproxy: invalid divert protocol") + errInvalidDivertIP = errors.New("tproxy: invalid divert IP") + errInvalidDivertPort = errors.New("tproxy: invalid divert port") +) + +func (c *tProxyUDPLikeConn) writeToWithDivert(pkt []byte, endpoint string) (int, error) { + divert := c.proxy.config.Divert[endpoint] + if divert == "" { + return 0, errMissingDivertEntry + } + idx := strings.LastIndex(divert, "/udp") + if idx < 0 { + return 0, errInvalidDivertProtocol + } + divert = divert[:idx] + addr, port, err := net.SplitHostPort(divert) + if err != nil { + return 0, err + } + ipAddr := net.ParseIP(addr) + if ipAddr == nil { + return 0, errInvalidDivertIP + } + portnum, err := strconv.Atoi(port) + if err != nil { + return 0, err + } + if portnum <= 0 || portnum > 65535 { + return 0, errInvalidDivertPort + } + udpAddr := &net.UDPAddr{ + IP: ipAddr, + Port: portnum, + } + return c.UDPLikeConn.WriteTo(pkt, udpAddr) +} + // // System resolver // @@ -276,6 +328,9 @@ func (d *tProxyDialer) DialContext(ctx context.Context, network, address string) case TProxyPolicyTCPRejectSYN: d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) return nil, netxlite.ECONNREFUSED + case TProxyPolicyDivert: + d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) + return d.dialContextWithDivert(ctx, network, endpoint) case TProxyPolicyHijackDNS: d.proxy.logger.Infof("tproxy: DialContext: %s/%s => %s", address, network, policy) address = d.proxy.dnsListener.LocalAddr().String() @@ -288,6 +343,29 @@ func (d *tProxyDialer) DialContext(ctx context.Context, network, address string) default: // nothing } + return d.doDialContext(ctx, network, address) +} + +func (d *tProxyDialer) dialContextWithDivert( + ctx context.Context, network, endpoint string) (net.Conn, error) { + divert := d.proxy.config.Divert[endpoint] + if divert == "" { + return nil, errMissingDivertEntry + } + idx := strings.LastIndex(divert, "/") + if idx < 0 { + return nil, errInvalidDivertProtocol + } + address := divert[:idx] + protocol := divert[idx+1:] + if protocol != "tcp" && protocol != "udp" { + return nil, errInvalidDivertProtocol + } + return d.doDialContext(ctx, network, address) +} + +func (d *tProxyDialer) doDialContext( + ctx context.Context, network, address string) (net.Conn, error) { conn, err := d.dialer.DialContext(ctx, network, address) if err != nil { return nil, err diff --git a/internal/netxlite/filtering/tproxy_test.go b/internal/netxlite/filtering/tproxy_test.go index aca4247..1fed6a8 100644 --- a/internal/netxlite/filtering/tproxy_test.go +++ b/internal/netxlite/filtering/tproxy_test.go @@ -224,6 +224,320 @@ func TestTProxyQUIC(t *testing.T) { t.Fatal("called") } }) + + t.Run("with divert policy", func(t *testing.T) { + t.Run("no divert entry", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errMissingDivertEntry) { + t.Fatal("unexpected err", err) + } + if count != 0 { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + + t.Run("invalid protocol", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "127.0.0.1:1234/udp": "127.0.0.1:1235", + }, + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errInvalidDivertProtocol) { + t.Fatal("unexpected err", err) + } + if count != 0 { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + + t.Run("invalid addrport", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "127.0.0.1:1234/udp": "127.0.0.1/udp", + }, + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + 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 || !strings.HasSuffix(err.Error(), "missing port in address") { + t.Fatal("unexpected err", err) + } + if count != 0 { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + + t.Run("invalid address", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "127.0.0.1:1234/udp": "localhost:1235/udp", + }, + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errInvalidDivertIP) { + t.Fatal("unexpected err", err) + } + if count != 0 { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + + t.Run("invalid port syntax", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "127.0.0.1:1234/udp": "127.0.0.1:xo/udp", + }, + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + 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 || !strings.HasSuffix(err.Error(), "invalid syntax") { + t.Fatal("unexpected err", err) + } + if count != 0 { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + + t.Run("invalid port value", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "127.0.0.1:1234/udp": "127.0.0.1:65536/udp", + }, + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errInvalidDivertPort) { + t.Fatal("unexpected err", err) + } + if count != 0 { + t.Fatal("unexpected number of bytes written") + } + if called { + t.Fatal("called") + } + }) + + t.Run("correct settings", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "127.0.0.1:1234/udp": "127.0.0.1:1235/udp", + }, + Endpoints: map[string]TProxyPolicy{ + "127.0.0.1:1234/udp": TProxyPolicyDivert, + }, + } + proxy, err := NewTProxy(config, log.Log) + if err != nil { + t.Fatal(err) + } + defer proxy.Close() + var realAddr *net.UDPAddr + proxy.listenUDP = func(network string, laddr *net.UDPAddr) (quicx.UDPLikeConn, error) { + return &mocks.QUICUDPLikeConn{ + MockWriteTo: func(p []byte, addr net.Addr) (int, error) { + realAddr = addr.(*net.UDPAddr) + 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 realAddr == nil || (*realAddr).Port != 1235 { + t.Fatal("invalid realAddr or invalid port value") + } + }) + }) }) } @@ -521,6 +835,111 @@ func TestTProxyDial(t *testing.T) { t.Fatal("expected nil conn here") } }) + + t.Run("with divert", func(t *testing.T) { + t.Run("with missing entry", func(t *testing.T) { + config := &TProxyConfig{ + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:53/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errMissingDivertEntry) { + t.Fatal("unexpected err", err) + } + if len(addrs) != 0 { + t.Fatal("expected no addrs here") + } + }) + + t.Run("with no divert protocol", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "8.8.8.8:53/udp": "8.8.8.8:54", + }, + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:53/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errInvalidDivertProtocol) { + t.Fatal("unexpected err", err) + } + if len(addrs) != 0 { + t.Fatal("expected no addrs here") + } + }) + + t.Run("with invalid divert protocol", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "8.8.8.8:53/udp": "8.8.8.8:54/antani", + }, + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:53/udp": TProxyPolicyDivert, + }, + } + 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 !errors.Is(err, errInvalidDivertProtocol) { + t.Fatal("unexpected err", err) + } + if len(addrs) != 0 { + t.Fatal("expected no addrs here") + } + }) + + t.Run("with all good", func(t *testing.T) { + config := &TProxyConfig{ + Divert: map[string]string{ + "8.8.8.8:53/udp": "8.8.8.8:54/udp", + }, + Endpoints: map[string]TProxyPolicy{ + "8.8.8.8:53/udp": TProxyPolicyDivert, + }, + } + 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") + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + addrs, err := resolver.LookupHost(ctx, "example.com") + if err == nil || err.Error() != netxlite.FailureGenericTimeoutError { + t.Fatal("unexpected err", err) + } + if len(addrs) != 0 { + t.Fatal("expected no addrs here") + } + }) + }) } func TestTProxyDNSCache(t *testing.T) {