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.
This commit is contained in:
Simone Basso 2021-11-03 00:29:14 +01:00 committed by GitHub
parent 675e3a5ba5
commit 851b9913fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 497 additions and 0 deletions

View File

@ -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

View File

@ -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) {