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:
parent
560b1a9a97
commit
11ccd16a0c
|
@ -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
|
||||
|
|
23
internal/netxlite/filtering/logger.go
Normal file
23
internal/netxlite/filtering/logger.go
Normal 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{})
|
||||
}
|
1
internal/netxlite/filtering/testdata/invalid.json
vendored
Normal file
1
internal/netxlite/filtering/testdata/invalid.json
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{
|
5
internal/netxlite/filtering/testdata/valid.json
vendored
Normal file
5
internal/netxlite/filtering/testdata/valid.json
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"Domains": {
|
||||
"x.org": "pass"
|
||||
}
|
||||
}
|
343
internal/netxlite/filtering/tproxy.go
Normal file
343
internal/netxlite/filtering/tproxy.go
Normal 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
|
||||
}
|
521
internal/netxlite/filtering/tproxy_test.go
Normal file
521
internal/netxlite/filtering/tproxy_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user