feat(filtering): add transparent proxy with censorship policies (#566)

This PR implements the core concept described at
https://github.com/ooni/probe/issues/1803#issuecomment-957323297
This commit is contained in:
Simone Basso 2021-11-02 21:52:32 +01:00 committed by GitHub
parent 560b1a9a97
commit 11ccd16a0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 905 additions and 1 deletions

View File

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

View File

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

View File

@ -0,0 +1 @@
{

View File

@ -0,0 +1,5 @@
{
"Domains": {
"x.org": "pass"
}
}

View File

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

View File

@ -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")
}
})
}