|
| 1 | +# Network Policy Filtering |
| 2 | + |
| 3 | +This package implements egress (outbound) traffic filtering for Lima's user-v2 networking using gVisor's netstack. |
| 4 | + |
| 5 | +## Architecture |
| 6 | + |
| 7 | +### Components |
| 8 | + |
| 9 | +1. **Policy** (`policy.go`) - YAML/JSON policy parsing and validation |
| 10 | +2. **DNS Tracker** (`dns.go`) - Tracks domain-to-IP mappings from DNS queries |
| 11 | +3. **DNS Snooper** (`dnssnooper.go`) - Intercepts DNS responses to populate tracker |
| 12 | +4. **Rules** (`rules.go`) - Converts policy rules to gVisor iptables rules |
| 13 | +5. **Filter** (`filter.go`) - Main entry point, wraps VirtualNetwork with filtering |
| 14 | + |
| 15 | +### Flow |
| 16 | + |
| 17 | +``` |
| 18 | +Policy File (YAML) |
| 19 | + ↓ |
| 20 | +LoadPolicy() → Policy struct |
| 21 | + ↓ |
| 22 | +virtualnetwork.New() → VirtualNetwork |
| 23 | + ↓ |
| 24 | +Filter(vn, policy) → FilteredVirtualNetwork |
| 25 | + ↓ |
| 26 | +BuildFilterTable() → gVisor iptables rules |
| 27 | + ↓ |
| 28 | +Packets evaluated by matchers: |
| 29 | + - DNS Snooper (tracks DNS) |
| 30 | + - Protocol Matcher (TCP/UDP/ICMP) |
| 31 | + - Port Matcher (destination ports) |
| 32 | + - IP Matcher (destination IPs/CIDRs) |
| 33 | + ↓ |
| 34 | +Allow or Drop |
| 35 | +``` |
| 36 | + |
| 37 | +### DNS Tracking via Snooping |
| 38 | + |
| 39 | +Domain-based rules require knowing which IPs belong to which domains. We achieve this by: |
| 40 | + |
| 41 | +1. Installing a DNS snooper matcher as the first iptables rule |
| 42 | +2. The snooper inspects all UDP port 53 source traffic (DNS responses) |
| 43 | +3. Parses DNS wire format to extract domain→IP mappings and TTL |
| 44 | +4. Updates the tracker with TTL-based expiration |
| 45 | +5. Snooper always returns `false` (never matches), just has side effects |
| 46 | + |
| 47 | +This approach is: |
| 48 | +- **Lightweight**: No extra DNS proxy needed |
| 49 | +- **Automatic**: Works with any DNS server |
| 50 | +- **Accurate**: Uses actual DNS TTL for cache expiration |
| 51 | + |
| 52 | +### Filter Table Structure |
| 53 | + |
| 54 | +For each policy, we build both IPv4 and IPv6 filter tables with OUTPUT chain rules: |
| 55 | + |
| 56 | +``` |
| 57 | +Rule 1: DNS Snooper (always returns false) |
| 58 | +Rule 2-N: Policy rules (sorted by priority) |
| 59 | +Rule N+1: Default DROP |
| 60 | +``` |
| 61 | + |
| 62 | +### Matchers |
| 63 | + |
| 64 | +Custom matchers implement `stack.Matcher` interface: |
| 65 | + |
| 66 | +- **protocolMatcher**: Matches transport protocol (TCP/UDP/ICMP/ICMPv6) |
| 67 | +- **portMatcher**: Matches destination port or port range |
| 68 | +- **ipMatcher**: Matches destination IP against CIDR list |
| 69 | +- **dnsSnooper**: Parses DNS responses, never matches |
| 70 | + |
| 71 | +### Memory Management |
| 72 | + |
| 73 | +- DNS tracker limited to 10,000 domains (configurable via `MaxDNSRecords`) |
| 74 | +- Expired entries cleaned every 5 minutes |
| 75 | +- When at capacity, expired entries cleaned first, then oldest evicted |
| 76 | +- Uses `sync.RWMutex` for concurrent access |
| 77 | + |
| 78 | +## Usage |
| 79 | + |
| 80 | +### Basic Usage |
| 81 | + |
| 82 | +```go |
| 83 | +// Load policy |
| 84 | +policy, err := filter.LoadPolicy("/path/to/policy.yaml") |
| 85 | + |
| 86 | +// Create virtual network |
| 87 | +vn, err := virtualnetwork.New(config) |
| 88 | + |
| 89 | +// Apply filtering |
| 90 | +fvn, err := filter.Filter(vn, policy) |
| 91 | + |
| 92 | +// Use the underlying virtual network |
| 93 | +network := fvn.VirtualNetwork() |
| 94 | +``` |
| 95 | + |
| 96 | +### Policy Format |
| 97 | + |
| 98 | +See `policy.yaml` for example or user documentation at: |
| 99 | +https://lima-vm.io/docs/config/network/policy/ |
| 100 | + |
| 101 | +## Testing |
| 102 | + |
| 103 | +- `filter_test.go` - Integration tests with gVisor stack |
| 104 | +- `dnssnooper_test.go` - DNS parsing and snooping tests |
| 105 | +- All tests use real gVisor stack, not mocks |
| 106 | + |
| 107 | +Run tests: |
| 108 | +```bash |
| 109 | +go test ./pkg/networks/usernet/filter/... |
| 110 | +``` |
| 111 | + |
| 112 | +## Limitations |
| 113 | + |
| 114 | +1. **DNS only**: Domain tracking only works with standard DNS (UDP port 53) |
| 115 | + - DoH (DNS over HTTPS) not supported |
| 116 | + - DoT (DNS over TLS) not supported |
| 117 | + |
| 118 | +2. **Egress only**: Only outbound traffic is filtered |
| 119 | + - Ingress filtering not implemented |
| 120 | + |
| 121 | +3. **ICMP support is incomplete**: no ICMP forwarder |
| 122 | + - See https://github.com/containers/gvisor-tap-vsock/issues/428 |
| 123 | + |
| 124 | +4. **Policy immutable**: Policy cannot be updated after Filter() is called |
| 125 | + - Requires network restart to apply new policy, by design |
| 126 | +` |
| 127 | + |
| 128 | +## Future Enhancements |
| 129 | + |
| 130 | +- [ ] Audit logging |
| 131 | +- [ ] Ingress filtering |
| 132 | +- [ ] ICMP Forwarder |
| 133 | + |
0 commit comments