quic block

This commit is contained in:
Ginder Singh
2026-03-19 00:49:09 -04:00
parent 41597609c8
commit ae37c56467
2 changed files with 254 additions and 30 deletions
+237 -30
View File
@@ -15,19 +15,29 @@ This module provides full packet capture capabilities for mobile VPN application
```
Mobile Apps (Browser, Games, etc)
VPN TUN Interface (10.0.0.2/24)
VPN TUN Interface
- Device: 10.0.0.2/24
- Gateway: 10.0.0.1
- DNS: 10.0.0.1 (ControlD)
PacketHandler (Read/Write/Protect)
gVisor Netstack (TCP/IP Stack)
├─→ DNS Filter (Port 53)
gVisor Netstack (10.0.0.1)
├─→ DNS Filter (dest port 53)
│ ├─→ DNS Bridge (transaction ID tracking)
│ └─→ ControlD DNS Proxy (localhost:5354)
├─→ TCP Forwarder
└─→ net.Dial("tcp") + protect(fd)
─→ UDP Forwarder
─→ net.Dial("udp") + protect(fd)
│ └─→ DoH Upstream (freedns.controld.com)
└─→ Protected Socket → Internet
─→ TCP Forwarder (non-DNS TCP traffic)
─→ net.Dial("tcp", destination)
│ ├─→ protect(fd) BEFORE connect()
│ └─→ io.Copy() bidirectional
└─→ UDP Forwarder (non-DNS UDP traffic)
├─→ net.Dial("udp", destination)
├─→ protect(fd) BEFORE connect()
└─→ Session tracking (60s timeout)
Real Network (WiFi/Cellular) - Protected Sockets
Real Network (WiFi/Cellular) - All Sockets Protected
```
## Key Components
@@ -98,10 +108,32 @@ dialer.Control = func(network, address string, c syscall.RawConn) error {
```
**All Protected Sockets:**
1. TCP forwarder sockets (user traffic)
2. UDP forwarder sockets (user traffic)
3. ControlD API HTTP sockets (api.controld.com)
4. DoH upstream sockets (freedns.controld.com)
1. **TCP forwarder sockets** - User HTTP/HTTPS traffic
2. **UDP forwarder sockets** - User games/video/VoIP traffic
3. **ControlD API sockets** - Configuration fetch (api.controld.com)
4. **DoH upstream sockets** - DNS resolution (freedns.controld.com)
**Timing is Critical:**
Protection must happen **DURING** socket creation, not after:
```go
// ✅ CORRECT - Protection happens BEFORE connect()
dialer := &net.Dialer{
Control: func(network, address string, c syscall.RawConn) error {
return c.Control(func(fd uintptr) {
protectSocket(int(fd)) // Called before connect() syscall
})
},
}
conn, _ := dialer.Dial("tcp", "google.com:443") // Socket already protected!
// ❌ WRONG - Protection happens AFTER connect()
conn, _ := net.Dial("tcp", "google.com:443") // SYN already sent through VPN!
rawConn.Control(func(fd uintptr) {
protectSocket(int(fd)) // TOO LATE - routing loop already started
})
```
### Outbound vs Return Packets
@@ -123,19 +155,164 @@ For inbound connections to the netstack:
Therefore, we dial `LocalAddress:LocalPort` (the destination).
## Packet Flow Examples
### DNS Query (Port 53)
```
1. App queries google.com
DNS query: 10.0.0.2:54321 → 10.0.0.1:53
2. VPN TUN captures packet
PacketHandler.readPacket() returns raw IP packet
3. DNS Filter detects port 53
Extracts UDP payload (DNS query bytes)
4. DNS Bridge processes query
- Parses DNS message (transaction ID: 12345)
- Stores in pending map
- Sends to query channel
5. DNS Handler receives query
- Forwards to ControlD proxy: localhost:5354
- ControlD applies policies and filters
- Queries DoH upstream (freedns.controld.com) via PROTECTED socket
- Returns filtered response
6. DNS Bridge matches response
- Finds pending query by transaction ID
- Sends response to waiting channel
7. DNS Filter builds response packet
- Wraps DNS bytes in UDP/IP packet
- Swaps src/dst: 10.0.0.1:53 → 10.0.0.2:54321
8. PacketHandler writes response to TUN
App receives DNS response with IP address
```
### TCP Connection (HTTP/HTTPS)
```
1. App connects to google.com:443
SYN packet: 10.0.0.2:12345 → 142.250.190.78:443
2. VPN TUN captures packet
PacketHandler.readPacket() returns raw IP packet
3. Netstack filters packet
- Checks source IP = 10.0.0.2 (outbound? YES)
- Not DNS (port != 53)
- Injects into gVisor netstack
4. gVisor calls TCP forwarder
TransportEndpointID:
- LocalAddress: 142.250.190.78:443 (destination)
- RemoteAddress: 10.0.0.2:12345 (source)
5. TCP Forwarder creates connection
- Creates gonet.TCPConn (TUN side)
- Dials net.Dial("tcp", "142.250.190.78:443")
- Protects socket BEFORE connect() → No routing loop!
- Connects via WiFi/Cellular (bypasses VPN)
6. Bidirectional copy
- TUN → Upstream: io.Copy(upstreamConn, tunConn)
- Upstream → TUN: io.Copy(tunConn, upstreamConn)
- All HTTP data flows through protected socket
7. Connection closes when either side finishes
```
### UDP Packet (Games/Video)
```
1. Game sends UDP packet
UDP: 10.0.0.2:54321 → game-server.com:9000
2. VPN TUN captures packet
PacketHandler.readPacket() returns raw IP packet
3. Netstack filters packet
- Checks source IP = 10.0.0.2 (outbound? YES)
- Not DNS (port != 53)
- Injects into gVisor netstack
4. gVisor calls UDP forwarder
TransportEndpointID:
- LocalAddress: game-server.com:9000 (destination)
- RemoteAddress: 10.0.0.2:54321 (source)
5. UDP Forwarder creates/reuses session
- Creates gonet.UDPConn (TUN side)
- Dials net.Dial("udp", "game-server.com:9000")
- Protects socket BEFORE connect() → No routing loop!
- Connects via WiFi/Cellular (bypasses VPN)
6. Bidirectional forwarding
- TUN → Upstream: Read from gonet, write to net.UDPConn
- Upstream → TUN: Read from net.UDPConn, write to gonet
- Session tracked with 60s idle timeout
7. Auto-cleanup after 60 seconds of inactivity
```
## VPN Configuration (Android)
```kotlin
val builder = Builder()
.setSession("ControlD VPN")
.addAddress("10.0.0.2", 24) // Device address
.addRoute("0.0.0.0", 0) // Route all traffic
.addDnsServer("10.0.0.1") // DNS to local TUN (ControlD)
.setMtu(1500)
vpnInterface = builder.establish()
```
**Why DNS = 10.0.0.1:**
- Apps query 10.0.0.1:53 (local TUN interface)
- Queries captured by DNS filter
- Filtered through ControlD policies
- Apps see "ControlD" as DNS provider (not Cloudflare/Google)
```
## Usage Example (Android)
```kotlin
// In VpnService
// In VpnService.startVpn()
// 1. Create config.toml (required by ctrld)
val configFile = File(filesDir, "config.toml")
configFile.createNewFile()
configFile.writeText("") // Empty config, uses defaults
// 2. Build VPN interface
val builder = Builder()
.setSession("ControlD VPN")
.addAddress("10.0.0.2", 24)
.addRoute("0.0.0.0", 0)
.addDnsServer("10.0.0.1") // CRITICAL: Use local TUN for DNS
.setMtu(1500)
vpnInterface = builder.establish()
// 3. Get TUN file descriptor streams
inputStream = FileInputStream(vpnInterface.fileDescriptor)
outputStream = FileOutputStream(vpnInterface.fileDescriptor)
// 4. Implement PacketAppCallback
val callback = object : PacketAppCallback {
override fun readPacket(): ByteArray {
// Read from TUN file descriptor
val length = inputStream.channel.read(buffer)
val length = inputStream.channel.read(readBuffer)
val packet = ByteArray(length)
readBuffer.position(0)
readBuffer.get(packet, 0, length)
return packet
}
override fun writePacket(packet: ByteArray) {
// Write to TUN file descriptor
outputStream.write(packet)
}
@@ -156,35 +333,65 @@ val callback = object : PacketAppCallback {
override fun macAddress(): String = "00:00:00:00:00:00"
}
// Create packet capture controller
val controller = Ctrld_library.newPacketCaptureController(callback)
// 5. Create and start packet capture controller
packetController = Ctrld_library.newPacketCaptureController(callback)
// Start packet capture
controller.startWithPacketCapture(
packetController.startWithPacketCapture(
callback,
"your-cd-uid",
"", "", // provision ID, custom hostname
filesDir.absolutePath,
"doh", // upstream protocol
2, // log level
"$filesDir/ctrld.log"
"your-cd-uid", // From ControlD dashboard
"", // Provision ID (optional)
"", // Custom hostname (optional)
filesDir.absolutePath, // Home directory
"doh", // Upstream protocol (doh/dot/os)
2, // Log level (0-5)
"${filesDir.absolutePath}/ctrld.log"
)
// Stop when done
controller.stop(false, 0)
// 6. Stop when disconnecting
packetController.stop(false, 0)
```
## Protocol Support
| Protocol | Support | Details |
|----------|---------|---------|
| **DNS** | ✅ Full | Filtered through ControlD proxy |
| **DNS** | ✅ Full | Filtered through ControlD proxy (UDP/TCP port 53) |
| **TCP** | ✅ Full | All ports, bidirectional forwarding |
| **UDP** | ✅ Full | All ports except 53, session tracking |
| **UDP** | ✅ Selective | All ports except 53, 80, 443 (see QUIC blocking) |
| **QUIC** | 🚫 Blocked | UDP ports 443 and 80 dropped to force TCP fallback |
| **ICMP** | ⚠️ Partial | Basic support (no forwarding yet) |
| **IPv4** | ✅ Full | Complete support |
| **IPv6** | ✅ Full | Complete support |
### QUIC Blocking
QUIC (Quick UDP Internet Connections) is blocked by dropping UDP packets on ports 443 and 80:
**Why Block QUIC:**
- QUIC bypasses traditional DNS lookups (uses Alt-Svc headers)
- Encrypts server name indication (SNI)
- Makes content filtering difficult
- Prevents some ControlD policies from working
**How It Works:**
```go
// In netstack.go readPackets()
if protocol == UDP {
if dstPort == 443 || dstPort == 80 {
// Drop QUIC/HTTP3 packets
// Apps automatically fallback to TCP (HTTP/2 or HTTP/1.1)
continue
}
}
```
**Result:**
- Chrome/apps attempt QUIC first
- QUIC packets dropped silently
- Apps fallback to TCP/TLS (HTTP/2)
- ControlD policies work correctly
- Slightly slower initial connection, then normal speed
## Performance
| Metric | Value |
+17
View File
@@ -287,6 +287,23 @@ func (nc *NetstackController) readPackets() {
// Drop it - return packets come through forwarder's upstream connection
continue
}
// Block QUIC protocol (UDP on port 443)
// QUIC runs over UDP and bypasses DNS, so we block it to force HTTP/2 or HTTP/3 over TCP
protocol := packet[9]
if protocol == 17 { // UDP
// Get IP header length
ihl := int(packet[0]&0x0f) * 4
if len(packet) >= ihl+4 {
// Parse UDP destination port (bytes 2-3 of UDP header)
dstPort := uint16(packet[ihl+2])<<8 | uint16(packet[ihl+3])
if dstPort == 443 || dstPort == 80 {
// Block QUIC (UDP/443) and HTTP/3 (UDP/80)
// Apps will fallback to TCP automatically
continue
}
}
}
}
// Create packet buffer