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