diff --git a/internal/client/dnsdial/doh.go b/internal/client/dnsdial/doh.go index f00d445..a9f3416 100644 --- a/internal/client/dnsdial/doh.go +++ b/internal/client/dnsdial/doh.go @@ -21,6 +21,7 @@ import ( "github.com/miekg/dns" "github.com/samosvalishe/free-turn-proxy/internal/logx" + "github.com/samosvalishe/free-turn-proxy/internal/netctl" // встроенные Mozilla CA roots для CGO_ENABLED=0 сборок (Android). _ "golang.org/x/crypto/x509roots/fallback" @@ -98,7 +99,7 @@ func newBootstrapTransport(endpoints []DohEndpoint) *http.Transport { for _, ep := range endpoints { bootstrap[ep.Hostname] = ep.BootstrapIPs } - dialer := &net.Dialer{Timeout: dohDialerTimeout, KeepAlive: dohDialerKeepAlive} + dialer := &net.Dialer{Timeout: dohDialerTimeout, KeepAlive: dohDialerKeepAlive, Control: netctl.Apply} return &http.Transport{ MaxIdleConns: 8, @@ -440,6 +441,7 @@ func newAppDialer(dial dialFunc) net.Dialer { Timeout: appDialerTimeout, KeepAlive: appDialerKeepAlive, Resolver: &net.Resolver{PreferGo: true, Dial: dial}, + Control: netctl.Apply, } } diff --git a/internal/netconn/netconn.go b/internal/netconn/netconn.go index bdf96fa..c5f49d3 100644 --- a/internal/netconn/netconn.go +++ b/internal/netconn/netconn.go @@ -12,6 +12,7 @@ import ( "time" "github.com/pion/transport/v4" + "github.com/samosvalishe/free-turn-proxy/internal/netctl" ) // DirectNet реализует transport.Net через стандартный net. @@ -34,32 +35,84 @@ type directTCPListener struct { *net.TCPListener } +// All socket-creating methods route through a net.Dialer / net.ListenConfig +// carrying netctl.Apply as Control so the host can protect each fd from the +// VPN tunnel. (net.DialUDP / net.ListenUDP take no Control, so we go via +// Dialer/ListenConfig and type-assert back to the concrete UDP/TCP conn.) func (DirectNet) ListenPacket(network string, address string) (net.PacketConn, error) { - return net.ListenPacket(network, address) //nolint:noctx + return (&net.ListenConfig{Control: netctl.Apply}).ListenPacket(context.Background(), network, address) } func (DirectNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) { - return net.ListenUDP(network, locAddr) -} - -func (DirectNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPListener, error) { - listener, err := net.ListenTCP(network, laddr) + addr := "" + if locAddr != nil { + addr = locAddr.String() + } + pc, err := (&net.ListenConfig{Control: netctl.Apply}).ListenPacket(context.Background(), network, addr) if err != nil { return nil, err } - return directTCPListener{listener}, nil + udp, ok := pc.(*net.UDPConn) + if !ok { + _ = pc.Close() + return nil, fmt.Errorf("netconn: expected *net.UDPConn, got %T", pc) + } + return udp, nil +} + +func (DirectNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPListener, error) { + addr := "" + if laddr != nil { + addr = laddr.String() + } + l, err := (&net.ListenConfig{Control: netctl.Apply}).Listen(context.Background(), network, addr) + if err != nil { + return nil, err + } + tl, ok := l.(*net.TCPListener) + if !ok { + _ = l.Close() + return nil, fmt.Errorf("netconn: expected *net.TCPListener, got %T", l) + } + return directTCPListener{tl}, nil } func (DirectNet) Dial(network, address string) (net.Conn, error) { - return net.Dial(network, address) //nolint:noctx + return (&net.Dialer{Control: netctl.Apply}).Dial(network, address) } func (DirectNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) { - return net.DialUDP(network, laddr, raddr) + d := &net.Dialer{Control: netctl.Apply} + if laddr != nil { + d.LocalAddr = laddr + } + c, err := d.Dial(network, raddr.String()) + if err != nil { + return nil, err + } + udp, ok := c.(*net.UDPConn) + if !ok { + _ = c.Close() + return nil, fmt.Errorf("netconn: expected *net.UDPConn, got %T", c) + } + return udp, nil } func (DirectNet) DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) { - return net.DialTCP(network, laddr, raddr) + d := &net.Dialer{Control: netctl.Apply} + if laddr != nil { + d.LocalAddr = laddr + } + c, err := d.Dial(network, raddr.String()) + if err != nil { + return nil, err + } + tcp, ok := c.(*net.TCPConn) + if !ok { + _ = c.Close() + return nil, fmt.Errorf("netconn: expected *net.TCPConn, got %T", c) + } + return tcp, nil } func (DirectNet) ResolveIPAddr(network, address string) (*net.IPAddr, error) { @@ -87,10 +140,16 @@ func (DirectNet) InterfaceByName(name string) (*transport.Interface, error) { } func (DirectNet) CreateDialer(dialer *net.Dialer) transport.Dialer { + if dialer.Control == nil { + dialer.Control = netctl.Apply + } return directDialer{Dialer: dialer} } func (DirectNet) CreateListenConfig(listenerConfig *net.ListenConfig) transport.ListenConfig { + if listenerConfig.Control == nil { + listenerConfig.Control = netctl.Apply + } return directListenConfig{ListenConfig: listenerConfig} } diff --git a/internal/netctl/netctl.go b/internal/netctl/netctl.go new file mode 100644 index 0000000..659460b --- /dev/null +++ b/internal/netctl/netctl.go @@ -0,0 +1,28 @@ +// Package netctl carries a process-global socket Control hook applied to every +// OUTBOUND socket the vendored free-turn client opens. The host installs it so +// those sockets are excluded from the VPN tunnel (Android VpnService.protect on +// the raw fd) — without it the client's own TURN/VK/DNS traffic loops back into +// the tunnel and hangs. This patch is local to our vendored copy: upstream has +// no such hook, and the dialers it creates (netconn.DirectNet, dnsdial, +// turndial) are routed through Apply below. +package netctl + +import "syscall" + +var control func(network, address string, c syscall.RawConn) error + +// SetControl installs the per-socket protector. Call before the client starts. +// nil (the default) makes Apply a no-op, so desktop builds are unaffected. +func SetControl(fn func(network, address string, c syscall.RawConn) error) { + control = fn +} + +// Apply is assigned as the Control func of every outbound net.Dialer / +// net.ListenConfig in the vendored client. It reads `control` at dial time so +// the host may register the protector before or after the client starts. +func Apply(network, address string, c syscall.RawConn) error { + if control != nil { + return control(network, address, c) + } + return nil +} diff --git a/internal/transport/turndial/turndial.go b/internal/transport/turndial/turndial.go index 3efa542..6fce244 100644 --- a/internal/transport/turndial/turndial.go +++ b/internal/transport/turndial/turndial.go @@ -17,6 +17,7 @@ import ( "github.com/pion/logging" "github.com/pion/turn/v5" "github.com/samosvalishe/free-turn-proxy/internal/netconn" + "github.com/samosvalishe/free-turn-proxy/internal/netctl" "github.com/samosvalishe/free-turn-proxy/internal/randx" ) @@ -83,10 +84,15 @@ func Open(ctx context.Context, cfg Config, peer *net.UDPAddr, user, pass, rawAdd closeConn func() error ) if cfg.TransportUDP { - c, derr := net.DialUDP("udp", nil, turnServerUDPAddr) //nolint:noctx + raw, derr := (&net.Dialer{Control: netctl.Apply}).Dial("udp", turnServerUDPAddr.String()) if derr != nil { return nil, fmt.Errorf("dial TURN (udp): %w", derr) } + c, ok := raw.(*net.UDPConn) + if !ok { + _ = raw.Close() + return nil, fmt.Errorf("turndial: expected *net.UDPConn, got %T", raw) + } turnConn = &netconn.ConnectedUDPConn{UDPConn: c} closeConn = c.Close } else { diff --git a/mobile/protect.go b/mobile/protect.go new file mode 100644 index 0000000..b6a4087 --- /dev/null +++ b/mobile/protect.go @@ -0,0 +1,30 @@ +package mobile + +import ( + "syscall" + + "github.com/samosvalishe/free-turn-proxy/internal/netctl" +) + +// Protector is implemented by the host to keep the client's own sockets out of +// a VPN tunnel it is feeding. On Android that is VpnService.protect(fd); on +// other tun-based hosts the equivalent. Without it the client's TURN / VK API / +// DNS traffic is routed back into the tunnel and the connection deadlocks. +type Protector interface { + // Protect is invoked once for every outbound socket the client opens, with + // the socket's raw file descriptor, before it connects. + Protect(fd int) bool +} + +// SetProtect installs the host socket protector. Pass nil to clear it (the +// default is a no-op, so desktop builds are unaffected). May be called before +// or after Start; it is read at dial time. +func SetProtect(p Protector) { + if p == nil { + netctl.SetControl(nil) + return + } + netctl.SetControl(func(_, _ string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { p.Protect(int(fd)) }) + }) +}