mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-05-15 00:50:25 +02:00
quic block
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user