Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
8 changes: 4 additions & 4 deletions mobile/mobile.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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()
Expand Down
106 changes: 84 additions & 22 deletions mobile/tun/fakedns_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ package tun
import (
"context"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
)

type FakeDNSProxy struct {
RealSocksAddr string
SocksUser string
SocksPass string
LocalPort int
dnsMap *DNSMapper
listener net.Listener
Expand All @@ -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 {
Expand Down Expand Up @@ -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...)
Expand Down Expand Up @@ -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...)
Expand Down
6 changes: 3 additions & 3 deletions mobile/tun/tun_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -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 {
Expand Down
Loading