Merge pull request #36 from smediainfo/feat/host-socket-protect

feat(mobile): add host socket protector (SetProtect) for tun/VPN hosts
This commit is contained in:
Anton
2026-07-01 19:30:28 +03:00
committed by GitHub
5 changed files with 137 additions and 12 deletions
+3 -1
View File
@@ -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,
}
}
+69 -10
View File
@@ -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}
}
+28
View File
@@ -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
}
+7 -1
View File
@@ -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 {
+30
View File
@@ -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)) })
})
}