mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-03-25 23:30:41 +01:00
240 lines
6.1 KiB
Markdown
240 lines
6.1 KiB
Markdown
# Netstack - Full Packet Capture for Mobile VPN
|
|
|
|
Complete TCP/UDP/DNS packet capture implementation using gVisor netstack for Android and iOS.
|
|
|
|
## Overview
|
|
|
|
Provides full packet capture for mobile VPN applications:
|
|
- **DNS filtering** through ControlD proxy
|
|
- **IP whitelisting** - only allows connections to DNS-resolved IPs
|
|
- **TCP forwarding** for all TCP traffic (with whitelist enforcement)
|
|
- **UDP forwarding** with session tracking (with whitelist enforcement)
|
|
- **Socket protection** to prevent routing loops
|
|
- **QUIC blocking** for better content filtering
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Mobile Apps → VPN TUN Interface → PacketHandler → gVisor Netstack
|
|
↓
|
|
├─→ DNS Filter (Port 53)
|
|
│ └─→ ControlD DNS Proxy
|
|
├─→ TCP Forwarder
|
|
│ └─→ net.Dial("tcp") + protect(fd)
|
|
└─→ UDP Forwarder
|
|
└─→ net.Dial("udp") + protect(fd)
|
|
↓
|
|
Real Network (Protected Sockets)
|
|
```
|
|
|
|
## Components
|
|
|
|
### DNS Filter (`dns_filter.go`)
|
|
Detects DNS packets on port 53, routes to ControlD proxy, and extracts resolved IPs.
|
|
|
|
### DNS Bridge (`dns_bridge.go`)
|
|
Tracks DNS queries by transaction ID with 5-second timeout.
|
|
|
|
### IP Tracker (`ip_tracker.go`)
|
|
Maintains whitelist of DNS-resolved IPs with 5-minute TTL.
|
|
|
|
### TCP Forwarder (`tcp_forwarder.go`)
|
|
Forwards TCP connections using gVisor's `tcp.NewForwarder()`. Blocks non-whitelisted IPs.
|
|
|
|
### UDP Forwarder (`udp_forwarder.go`)
|
|
Forwards UDP packets with 30-second read deadline. Blocks non-whitelisted IPs.
|
|
|
|
### Packet Handler (`packet_handler.go`)
|
|
Interface for TUN I/O and socket protection.
|
|
|
|
### Netstack Controller (`netstack.go`)
|
|
Manages gVisor stack and coordinates all components.
|
|
|
|
## Socket Protection
|
|
|
|
Critical for preventing routing loops:
|
|
|
|
```go
|
|
// Protection happens BEFORE connect()
|
|
dialer.Control = func(network, address string, c syscall.RawConn) error {
|
|
return c.Control(func(fd uintptr) {
|
|
protectSocket(int(fd))
|
|
})
|
|
}
|
|
```
|
|
|
|
**All protected sockets:**
|
|
- TCP/UDP forwarder sockets (user traffic)
|
|
- ControlD API sockets (api.controld.com)
|
|
- DoH upstream sockets (freedns.controld.com)
|
|
|
|
## Platform Configuration
|
|
|
|
### Android
|
|
|
|
```kotlin
|
|
Builder()
|
|
.addAddress("10.0.0.2", 24)
|
|
.addRoute("0.0.0.0", 0)
|
|
.addDnsServer("10.0.0.1")
|
|
.setMtu(1500)
|
|
|
|
override fun protectSocket(fd: Long) {
|
|
protect(fd.toInt()) // VpnService.protect()
|
|
}
|
|
|
|
// DNS Proxy: 0.0.0.0:5354
|
|
```
|
|
|
|
### iOS
|
|
|
|
```swift
|
|
NEIPv4Settings(addresses: ["10.0.0.2"], ...)
|
|
NEDNSSettings(servers: ["10.0.0.1"])
|
|
includedRoutes = [NEIPv4Route.default()]
|
|
|
|
func protectSocket(_ fd: Int) throws {
|
|
// No action needed - iOS Network Extension sockets
|
|
// automatically bypass VPN tunnel
|
|
}
|
|
|
|
// DNS Proxy: 127.0.0.1:53
|
|
```
|
|
|
|
**Note:** iOS Network Extensions run in separate process - sockets automatically bypass VPN.
|
|
|
|
## Protocol Support
|
|
|
|
| Protocol | Support |
|
|
|----------|---------|
|
|
| DNS (UDP/TCP port 53) | ✅ Full |
|
|
| TCP (all ports) | ✅ Full |
|
|
| UDP (except 53, 80, 443) | ✅ Full |
|
|
| QUIC (UDP/443, UDP/80) | 🚫 Blocked |
|
|
| ICMP | ⚠️ Partial |
|
|
| IPv4 | ✅ Full |
|
|
| IPv6 | ✅ Full |
|
|
|
|
## QUIC Blocking
|
|
|
|
Drops UDP packets on ports 443 and 80 to force TCP fallback:
|
|
|
|
**Why:**
|
|
- Better DNS filtering (QUIC bypasses DNS)
|
|
- Visible SNI for content filtering
|
|
- Consistent ControlD policy enforcement
|
|
|
|
**Result:**
|
|
- Apps automatically fallback to TCP/TLS
|
|
- No user-visible errors
|
|
- Slightly slower initial connection, then normal
|
|
|
|
## IP Blocking (DNS Bypass Prevention)
|
|
|
|
Enforces whitelist approach: ONLY allows connections to IPs resolved through ControlD DNS.
|
|
|
|
**How it works:**
|
|
1. DNS responses are parsed to extract A and AAAA records
|
|
2. Resolved IPs are tracked in memory whitelist for 5 minutes
|
|
3. TCP/UDP connections to **non-whitelisted** IPs are **BLOCKED**
|
|
4. Only IPs that went through DNS resolution are allowed
|
|
|
|
**Why:**
|
|
- Prevents DNS bypass via hardcoded/cached IPs
|
|
- Ensures ALL traffic must go through ControlD DNS first
|
|
- Blocks apps that try to skip DNS filtering
|
|
- Enforces strict ControlD policy compliance
|
|
|
|
**Example:**
|
|
```
|
|
✅ ALLOWED: App queries "example.com" → 93.184.216.34 → connects to 93.184.216.34
|
|
❌ BLOCKED: App connects directly to 1.2.3.4 (not resolved via DNS)
|
|
```
|
|
|
|
**Components:**
|
|
- `ip_tracker.go` - Manages whitelist of DNS-resolved IPs with TTL
|
|
- `dns_filter.go` - Extracts IPs from DNS responses for whitelist
|
|
- `tcp_forwarder.go` - Allows only whitelisted IPs, blocks others
|
|
- `udp_forwarder.go` - Allows only whitelisted IPs, blocks others
|
|
|
|
## Usage (Android)
|
|
|
|
```kotlin
|
|
// Create callback
|
|
val callback = object : PacketAppCallback {
|
|
override fun readPacket(): ByteArray { ... }
|
|
override fun writePacket(packet: ByteArray) { ... }
|
|
override fun protectSocket(fd: Long) {
|
|
protect(fd.toInt())
|
|
}
|
|
override fun closePacketIO() { ... }
|
|
override fun exit(s: String) { ... }
|
|
override fun hostname(): String = "android-device"
|
|
override fun lanIp(): String = "10.0.0.2"
|
|
override fun macAddress(): String = "00:00:00:00:00:00"
|
|
}
|
|
|
|
// Create controller
|
|
val controller = Ctrld_library.newPacketCaptureController(callback)
|
|
|
|
// Start
|
|
controller.startWithPacketCapture(
|
|
callback,
|
|
cdUID,
|
|
"", "",
|
|
filesDir.absolutePath,
|
|
"doh",
|
|
2,
|
|
"$filesDir/ctrld.log"
|
|
)
|
|
|
|
// Stop
|
|
controller.stop(false, 0)
|
|
```
|
|
|
|
## Usage (iOS)
|
|
|
|
```swift
|
|
// DNS-only mode
|
|
let proxy = LocalProxy()
|
|
proxy.start(
|
|
cUID: cdUID,
|
|
deviceName: UIDevice.current.name,
|
|
upstreamProto: "doh",
|
|
logLevel: 3,
|
|
provisionID: ""
|
|
)
|
|
|
|
// Firewall mode (full capture)
|
|
let proxy = LocalProxy()
|
|
proxy.startFirewall(
|
|
cUID: cdUID,
|
|
deviceName: UIDevice.current.name,
|
|
upstreamProto: "doh",
|
|
logLevel: 3,
|
|
provisionID: "",
|
|
packetFlow: packetFlow
|
|
)
|
|
```
|
|
|
|
## Requirements
|
|
|
|
- **Android**: API 24+ (Android 7.0+)
|
|
- **iOS**: iOS 12.0+
|
|
- **Go**: 1.23+
|
|
- **gVisor**: v0.0.0-20240722211153-64c016c92987
|
|
|
|
## Files
|
|
|
|
- `packet_handler.go` - TUN I/O interface
|
|
- `netstack.go` - gVisor controller
|
|
- `dns_filter.go` - DNS packet detection and IP extraction
|
|
- `dns_bridge.go` - Transaction tracking
|
|
- `ip_tracker.go` - DNS-resolved IP whitelist with TTL
|
|
- `tcp_forwarder.go` - TCP forwarding with whitelist enforcement
|
|
- `udp_forwarder.go` - UDP forwarding with whitelist enforcement
|
|
|
|
## License
|
|
|
|
Same as parent ctrld project.
|