Skip to content

Commit 29a1de2

Browse files
committed
fix(vpn): resolve ERR_QUIC_PROTOCOL_ERROR and DNS failures on Android 16 TUN mode (issue #32)
Root cause: FakeDNSProxy forwarded non-DNS UDP (QUIC) to upstream SOCKS5 UDP_ASSOCIATE, which rejects non-53 targets and closes the association, black-holing subsequent DNS queries. IPv6 ::/0 was also routed into the TUN without an IPv6 DNS server to advertise, so IPv6 QUIC had no path. Changes (uniform across all Android versions for easier debugging): * MasterDnsVpnService.kt - Drop IPv6 ::/0 route and fc00::1 address from VpnService.Builder; FakeDNSProxy has no IPv6 DNS to advertise and upstream SOCKS5 UDP_ASSOCIATE rejects non-53 traffic, so ::/0 only black-holes IPv6 QUIC. Re-enablement conditions documented in ponytail comment. - setBlocking(true) on the TUN fd: tun2socks/gVisor fdbased reads require a blocking fd to avoid busy-loop polling. - Add NetworkCallback.onLost override calling setUnderlyingNetworks(null) so Android 16 does not retain stale underlying-network state after onLost fires (onAvailable only set it, never reset). - TUN MTU 1500 to 1400 on both Builder.setMtu and the startTunBridge call, leaving headroom for SOCKS5/transport overhead so QUIC datagrams do not exceed the upstream path MTU and black-hole. * mobile/mobile.go - StartTun engine.Key.MTU 1500 to 1400 to match the Android side. * mobile/tun/fakedns_proxy.go - Stop forwarding non-53 UDP to upstream. Browsers QUIC probes now time out fast and fall back to TCP instead of hanging on a rejected UDP_ASSOCIATE that would also kill following DNS queries. - Parse atyp==4 (IPv6) BND address in the UDP_ASSOCIATE reply so the forward-compat path is correct when IPv6 is re-enabled later.
1 parent 1c04a2f commit 29a1de2

3 files changed

Lines changed: 27 additions & 20 deletions

File tree

android/app/src/main/java/com/masterdns/vpn/service/MasterDnsVpnService.kt

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -287,21 +287,17 @@ class MasterDnsVpnService : VpnService() {
287287

288288
val builder = Builder()
289289
.setSession(getString(R.string.app_name))
290-
.setMtu(1500)
290+
.setMtu(1400)
291291
.addAddress(if (globalSettings.fakeDnsEnabled) "172.19.0.1" else "10.0.0.2", if (globalSettings.fakeDnsEnabled) 30 else 32)
292292
.addRoute("0.0.0.0", 0)
293+
.setBlocking(true) // tun2socks/gVisor requires blocking fd reads
293294

294-
// Prevent IPv6 DNS leaks: if we don't route IPv6 and provide an IPv6 DNS,
295-
// Android may leak DNS requests to the cellular network's IPv6 DNS server,
296-
// resulting in hijacked IPs like 10.10.34.36 from the ISP.
297-
try {
298-
builder.addAddress("fc00::1", 128)
299-
builder.addRoute("::", 0)
300-
// We don't add fc00::1 to dns servers here because Android sometimes rejects it,
301-
// but routing ::/0 forces all IPv6 traffic (including DNS) into the TUN.
302-
} catch (e: Exception) {
303-
VpnManager.appendLog("IPv6 routing skipped: ${e.message}")
304-
}
295+
// ponytail: IPv6 NOT routed into the TUN across all Android versions.
296+
// FakeDNSProxy only has IPv4 DNS to advertise and upstream SOCKS5 UDP_ASSOCIATE
297+
// rejects non-53 traffic (fakedns_proxy.go:339-345, socks_manager.go:665), so
298+
// routing ::/0 black-holes IPv6 QUIC on every Android version, not just 16+.
299+
// Re-enable when FakeDNSProxy parses atyp==4 BND AND an IPv6 DNS is configured
300+
// AND upstream SOCKS5 carries non-DNS IPv6 UDP.
305301

306302
vpnDnsServers.forEach { builder.addDnsServer(it) }
307303
if (globalSettings.fakeDnsEnabled) {
@@ -368,7 +364,7 @@ class MasterDnsVpnService : VpnService() {
368364

369365
if (globalSettings.fakeDnsEnabled) {
370366
VpnManager.appendLog("Starting DNS-aware TUN bridge...")
371-
mobile.Mobile.startTunBridge(vpnInterface!!.fd.toLong(), 1500L, "127.0.0.1:$socksPort")
367+
mobile.Mobile.startTunBridge(vpnInterface!!.fd.toLong(), 1400L, "127.0.0.1:$socksPort")
372368
tunBridgeActive = true
373369
VpnManager.appendLog("DNS-aware TUN bridge started")
374370
} else {
@@ -384,10 +380,16 @@ class MasterDnsVpnService : VpnService() {
384380
val caps = cm.getNetworkCapabilities(network)
385381
if (caps == null || caps.hasTransport(android.net.NetworkCapabilities.TRANSPORT_VPN)) return
386382
if (isStopping) return
387-
383+
388384
VpnManager.appendLog("Underlying network changed, updating VPN underlying network...")
389385
setUnderlyingNetworks(arrayOf(network))
390386
}
387+
388+
override fun onLost(network: android.net.Network) {
389+
if (isStopping) return
390+
VpnManager.appendLog("Underlying network lost, resetting VPN underlying network...")
391+
setUnderlyingNetworks(null)
392+
}
391393
}
392394
try {
393395
val request = android.net.NetworkRequest.Builder()

mobile/mobile.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func handleTracking(c net.Conn, realProxyAddr string) {
109109
defer server.Close()
110110

111111
tcClient := &trackingConn{
112-
Conn: c,
112+
Conn: c,
113113
onRead: func(n int64) { atomic.AddInt64(&trackedUp, n) },
114114
onWrite: func(n int64) { atomic.AddInt64(&trackedDown, n) },
115115
}
@@ -210,7 +210,9 @@ func StartTun(fd int64, proxyAddr string) {
210210
key := &engine.Key{
211211
Proxy: "socks5://" + proxyAddr,
212212
Device: fmt.Sprintf("fd://%d", safeFd),
213-
MTU: 1500,
213+
// ponytail: 1400 leaves headroom for SOCKS5/transport overhead so QUIC
214+
// datagrams don't exceed upstream path MTU and black-hole (issue #32).
215+
MTU: 1400,
214216
}
215217

216218
engine.Insert(key)
@@ -276,7 +278,6 @@ func StopClient() {
276278
StopTunBridge()
277279
}
278280

279-
280281
// IsRunning returns true if the client is currently running.
281282
func IsRunning() bool {
282283
mu.Lock()

mobile/tun/fakedns_proxy.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ func (p *FakeDNSProxy) handleUDPAssociate(tcpConn net.Conn, atyp byte, targetAdd
244244
var realUdpAddr *net.UDPAddr
245245
if replyHeader[3] == 1 {
246246
realUdpAddr = &net.UDPAddr{IP: net.IP(bndAddr), Port: int(binary.BigEndian.Uint16(bndPortBuf))}
247+
} else if replyHeader[3] == 4 {
248+
realUdpAddr = &net.UDPAddr{IP: net.IP(bndAddr), Port: int(binary.BigEndian.Uint16(bndPortBuf))}
247249
}
248250
if realUdpAddr != nil && realUdpAddr.IP.IsUnspecified() {
249251
host, _, _ := net.SplitHostPort(p.RealSocksAddr)
@@ -334,9 +336,11 @@ func (p *FakeDNSProxy) handleUDPAssociate(tcpConn net.Conn, atyp byte, targetAdd
334336
continue
335337
}
336338

337-
if realUdpAddr != nil {
338-
localUdp.WriteToUDP(buf[:n], realUdpAddr)
339-
}
339+
// ponytail: non-DNS UDP (QUIC, etc.) dropped, not forwarded.
340+
// Upstream SOCKS5 UDP_ASSOCIATE rejects non-53 targets
341+
// (socks_manager.go:665), forwarding would close the association
342+
// and break subsequent DNS queries. Dropping lets the browser's
343+
// QUIC probe time out fast and fall back to TCP (issue #32).
340344
}
341345
}()
342346

0 commit comments

Comments
 (0)