diff --git a/android/app/src/main/java/com/gooserelay/gooserelayvpn/service/GooseRelayVpnService.kt b/android/app/src/main/java/com/gooserelay/gooserelayvpn/service/GooseRelayVpnService.kt index 1d52b4a..e962454 100644 --- a/android/app/src/main/java/com/gooserelay/gooserelayvpn/service/GooseRelayVpnService.kt +++ b/android/app/src/main/java/com/gooserelay/gooserelayvpn/service/GooseRelayVpnService.kt @@ -340,10 +340,27 @@ class GooseRelayVpnService : VpnService() { // TUN module is included in mobile package val tunClass = Class.forName("mobile.Mobile") - val startMethod = tunClass.getMethod("startTunBridge", Long::class.java, Long::class.java, String::class.java) + val startMethod = tunClass.getMethod( + "startTunBridge", + Long::class.java, + Long::class.java, + String::class.java, + String::class.java, + String::class.java, + ) - // Start TUN bridge: fd, mtu, socksAddr - startMethod.invoke(null, vpnInterface!!.fd.toLong(), 1500L, "127.0.0.1:$socksPort") + // Start TUN bridge: fd, mtu, socksAddr, socksUser, socksPass + // The Go core's SOCKS5 server enforces user/pass auth when + // both fields are non-empty; we forward them so the FakeDNS + // proxy can negotiate through it. + startMethod.invoke( + null, + vpnInterface!!.fd.toLong(), + 1500L, + "127.0.0.1:$socksPort", + profile.socksUser, + profile.socksPass, + ) tunBridgeActive = true VpnManager.appendLog("Go TUN bridge started (DNS will be resolved remotely)") diff --git a/mobile/mobile.go b/mobile/mobile.go index 3a7efbb..d2b8cb0 100644 --- a/mobile/mobile.go +++ b/mobile/mobile.go @@ -303,12 +303,12 @@ func StopTun() { // TUN Bridge wrapper functions (calls mobile/tun subpackage) // StartTunBridge starts the TUN bridge with DNS interception using FakeDNS proxy. -func StartTunBridge(tunFd int64, mtu int64, socksAddr string) error { - proxyAddr, err := tun.StartFakeDNSProxy(socksAddr) +func StartTunBridge(tunFd int64, mtu int64, socksAddr, socksUser, socksPass string) error { + proxyAddr, err := tun.StartFakeDNSProxy(socksAddr, socksUser, socksPass) if err != nil { return err } - + safeFd := dupFd(int(tunFd)) key := &engine.Key{ @@ -325,7 +325,7 @@ func StartTunBridge(tunFd int64, mtu int64, socksAddr string) error { engine.Start() tunOwnedFd = int32(safeFd) engineMu.Unlock() - + mu.Lock() tunBridgeRunning = true mu.Unlock() diff --git a/mobile/tun/fakedns_proxy.go b/mobile/tun/fakedns_proxy.go index 99c61f2..051ad4a 100644 --- a/mobile/tun/fakedns_proxy.go +++ b/mobile/tun/fakedns_proxy.go @@ -3,6 +3,7 @@ package tun import ( "context" "encoding/binary" + "fmt" "io" "net" "sync" @@ -10,6 +11,8 @@ import ( type FakeDNSProxy struct { RealSocksAddr string + SocksUser string + SocksPass string LocalPort int dnsMap *DNSMapper listener net.Listener @@ -18,16 +21,91 @@ type FakeDNSProxy struct { wg sync.WaitGroup } -func NewFakeDNSProxy(realSocksAddr string, dnsMap *DNSMapper) *FakeDNSProxy { +func NewFakeDNSProxy(realSocksAddr, socksUser, socksPass string, dnsMap *DNSMapper) *FakeDNSProxy { ctx, cancel := context.WithCancel(context.Background()) return &FakeDNSProxy{ RealSocksAddr: realSocksAddr, + SocksUser: socksUser, + SocksPass: socksPass, dnsMap: dnsMap, ctx: ctx, cancel: cancel, } } +// dialRealSocks opens a TCP connection to the upstream SOCKS5 server and +// completes the auth handshake. If [SocksUser] and [SocksPass] are both +// set, the user/pass method (0x02) is offered; otherwise NO_AUTH (0x00). +// Returns the negotiated upstream connection, or nil + an error. +func (p *FakeDNSProxy) dialRealSocks() (net.Conn, error) { + realConn, err := net.Dial("tcp", p.RealSocksAddr) + if err != nil { + return nil, err + } + + // Greeting: offer NO_AUTH and (if configured) USERNAME/PASSWORD. + // RFC 1928 + RFC 1929. + var greeting []byte + if p.SocksUser != "" && p.SocksPass != "" { + greeting = []byte{5, 2, 0, 2} // VER=5, NMETHODS=2, NO_AUTH, USER/PASS + } else { + greeting = []byte{5, 1, 0} // VER=5, NMETHODS=1, NO_AUTH + } + if _, err := realConn.Write(greeting); err != nil { + realConn.Close() + return nil, err + } + + resp := make([]byte, 2) + if _, err := io.ReadFull(realConn, resp); err != nil { + realConn.Close() + return nil, err + } + if resp[0] != 5 { + realConn.Close() + return nil, fmt.Errorf("upstream SOCKS5 version mismatch: %d", resp[0]) + } + + switch resp[1] { + case 0: + // NO_AUTH selected. Done. + return realConn, nil + case 2: + // USERNAME/PASSWORD selected — proceed with RFC 1929 sub-negotiation. + if p.SocksUser == "" || p.SocksPass == "" { + realConn.Close() + return nil, fmt.Errorf("upstream requires auth but no credentials configured") + } + user := []byte(p.SocksUser) + pass := []byte(p.SocksPass) + if len(user) > 255 || len(pass) > 255 { + realConn.Close() + return nil, fmt.Errorf("socks credentials too long") + } + req := []byte{1, byte(len(user))} + req = append(req, user...) + req = append(req, byte(len(pass))) + req = append(req, pass...) + if _, err := realConn.Write(req); err != nil { + realConn.Close() + return nil, err + } + authResp := make([]byte, 2) + if _, err := io.ReadFull(realConn, authResp); err != nil { + realConn.Close() + return nil, err + } + if authResp[1] != 0 { + realConn.Close() + return nil, fmt.Errorf("socks auth rejected (status %d)", authResp[1]) + } + return realConn, nil + default: + realConn.Close() + return nil, fmt.Errorf("upstream selected unsupported auth method %d", resp[1]) + } +} + func (p *FakeDNSProxy) Start() (string, error) { l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -139,22 +217,14 @@ func (p *FakeDNSProxy) handleConnection(conn net.Conn) { return } - // Dial Real SOCKS - realConn, err := net.Dial("tcp", p.RealSocksAddr) + // Dial Real SOCKS (with auth if configured) + realConn, err := p.dialRealSocks() if err != nil { - conn.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0}) + _, _ = conn.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0}) return } defer realConn.Close() - if _, err := realConn.Write([]byte{5, 1, 0}); err != nil { - return - } - authResp := make([]byte, 2) - if _, err := io.ReadFull(realConn, authResp); err != nil { - return - } - req := []byte{5, 1, 0, atyp} req = append(req, targetAddr...) req = append(req, targetPort...) @@ -195,21 +265,13 @@ func (p *FakeDNSProxy) handleConnection(conn net.Conn) { } func (p *FakeDNSProxy) handleUDPAssociate(tcpConn net.Conn, atyp byte, targetAddr []byte, targetPort []byte) { - realConn, err := net.Dial("tcp", p.RealSocksAddr) + realConn, err := p.dialRealSocks() if err != nil { - tcpConn.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0}) + _, _ = tcpConn.Write([]byte{5, 1, 0, 1, 0, 0, 0, 0, 0, 0}) return } defer realConn.Close() - if _, err := realConn.Write([]byte{5, 1, 0}); err != nil { - return - } - authResp := make([]byte, 2) - if _, err := io.ReadFull(realConn, authResp); err != nil { - return - } - req := []byte{5, 3, 0, atyp} req = append(req, targetAddr...) req = append(req, targetPort...) diff --git a/mobile/tun/tun_api.go b/mobile/tun/tun_api.go index 3169cda..1aae9c7 100644 --- a/mobile/tun/tun_api.go +++ b/mobile/tun/tun_api.go @@ -16,7 +16,7 @@ var ( sharedDnsMap *DNSMapper ) -func StartFakeDNSProxy(socksAddr string) (string, error) { +func StartFakeDNSProxy(socksAddr, socksUser, socksPass string) (string, error) { bridgeMu.Lock() defer bridgeMu.Unlock() @@ -28,10 +28,10 @@ func StartFakeDNSProxy(socksAddr string) (string, error) { return "", fmt.Errorf("socksAddr cannot be empty") } - log.Printf("[TUN-API] Starting FakeDNS SOCKS5 proxy pointing to %s", socksAddr) + log.Printf("[TUN-API] Starting FakeDNS SOCKS5 proxy pointing to %s (auth=%v)", socksAddr, socksUser != "") sharedDnsMap = NewDNSMapper() - proxy := NewFakeDNSProxy(socksAddr, sharedDnsMap) + proxy := NewFakeDNSProxy(socksAddr, socksUser, socksPass, sharedDnsMap) addr, err := proxy.Start() if err != nil {