Networking
Bridge, CNI, DNS, iptables, port mapping, rootless
Networking
Doki's networking stack provides bridge networks, CNI plugin support, port mapping, and an internal DNS server.
Network Types
| Type | Description | Driver |
|---|---|---|
| bridge | Default doki0 bridge with NAT, DNS, port mapping |
Linux bridge + iptables |
| host | Share host network namespace | (no driver) |
| none | Loopback only | (no driver) |
| overlay | Multi-host (planned) | vxlan |
| macvlan | Direct host NIC access | macvlan |
| ipvlan | L3 isolation | ipvlan |
Default Bridge: doki0
On first start, the daemon creates a Linux bridge named doki0 with the following config:
| Property | Default |
|---|---|
| Subnet | 10.0.0.0/24 |
| Gateway | 10.0.0.1 |
| MTU | 1500 |
| IP allocation | Sequential (.2, .3, ...) |
| iptables | MASQUERADE on outbound, DNAT on port forward |
Override in config.json:
{
"network": {
"bridge": "doki0",
"default_subnet": "10.1.0.0/24",
"mtu": 1500,
"ipv6": false
}
}
Container Attachment
When a container is started with --network bridge, the daemon:
- Creates a veth pair (
veth<random>↔eth0inside container) - Attaches the host-side veth to
doki0 - Assigns an IP from the subnet
- Sets up iptables rules
- Registers the container name in the internal DNS
Host Network
--network host skips the bridge and gives the container the host's network namespace. The container sees all host interfaces and IPs. No port mapping needed (-p is a no-op).
Performance is the best of all modes. Security: least isolated — the container can sniff all host traffic.
None
--network none gives the container only lo. No external network. Useful for batch processing, security-sensitive workloads.
Port Mapping
Syntax
-p HOST_IP:HOST_PORT:CONTAINER_PORT/PROTOCOL -p HOST_PORT:CONTAINER_PORT -p CONTAINER_PORT # random host port (use -P to publish all EXPOSE)
Examples
# Map host 8080 to container 80 doki run -p 8080:80 nginx:alpine # Bind to specific host IP doki run -p 127.0.0.1:8080:80 nginx:alpine # Multiple protocols doki run -p 8080:80/tcp -p 8080:80/udp my-server:latest # Publish all EXPOSEd ports doki run -P nginx:alpine # Port range doki run -p 8080-8090:80 my-server:latest
How it works (rootful)
iptables -t nat -A DOKI -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.2:80iptables -t nat -A POSTROUTING -s 10.0.0.2 -j MASQUERADE(for return path)socatfor the actual TCP proxy in rootless mode
iptables DNAT
The DNAT rule construction in pkg/network/manager.go was using strings.Split and missing the -A (append) flag in v0.9.1:
- args := strings.Split("OUTPUT -p tcp --dport 8080 -j DNAT --to-destination 10.0.0.2:80", " ")
- exec.Command("iptables", args...).Run() // error discarded
+ args := []string{
+ "-A", "OUTPUT",
+ "-p", "tcp",
+ "--dport", "8080",
+ "-j", "DNAT",
+ "--to-destination", "10.0.0.2:80",
+ }
+ out, err := exec.Command("iptables", args...).CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("iptables DNAT: %s: %w", out, err)
+ }
Two things fixed:
-Aflag: v0.9.1 hadOUTPUTas the first arg, which iptables interpreted as the table name. The fix uses[]stringand includes-Acorrectly.- Error check: v0.9.1 called
.Run()and discarded the error. The fix uses.CombinedOutput()and wraps the error.
The DOKI chain is now also auto-created in pkg/network/cni.go:ensureChains() (idempotent — safe to call on every container start).
Port forwarding
The rootless socat proxy was connecting to localhost:containerPort instead of containerIP:containerPort:
- socatArgs := []string{
- "TCP-LISTEN:8080,reuseaddr,fork",
- "TCP:localhost:80", // ← wrong: localhost from host ≠ container
- }
+ socatArgs := []string{
+ "TCP-LISTEN:8080,reuseaddr,fork",
+ "TCP:10.0.0.2:80", // ← container bridge IP
+ }
UDP support
UDP port forwarding is now supported via socat -u:
if port.Type == "udp" {
socatArgs = append(socatArgs[:2],
append([]string{"UDP-LISTEN:8080,reuseaddr,fork"},
"UDP:10.0.0.2:80")...)
}
Internal DNS
Doki runs an internal DNS server that handles:
- Inter-container name resolution (
db→10.0.0.2) - A records (IPv4)
- AAAA records (IPv6)
- PTR records (reverse DNS)
- Forwarding to upstream resolvers
Architecture
%%{init: {'theme':'base', 'themeVariables':{'primaryColor':'#1e1e2e','primaryTextColor':'#cdd6f4','primaryBorderColor':'#89b4fa','lineColor':'#89b4fa','fontFamily':'ui-monospace,SFMono-Regular,Menlo,Monaco,monospace'}}}%%
flowchart TD
Resolv["Container /etc/resolv.conf<br/>nameserver 127.0.0.11"]
Doki["Doki internal DNS<br/>:53 Linux · :8053 Android"]
Local["Local<br/>container-name → bridge IP"]
Upstream["Upstream<br/>/etc/resolv.conf · getprop net.dns* · 8.8.8.8"]
Internet(("Internet"))
Resolv -->|"A · AAAA · PTR"| Doki
Doki --> Local
Doki --> Upstream
Upstream --> Internet
Defaults
| Platform | Default listen | Why |
|---|---|---|
| Linux | 127.0.0.11:53 |
Standard unprivileged port |
| Android (Termux) | 127.0.0.11:8053 |
Port 53 blocked by SELinux |
| macOS | not used (ModeNative) | No bridge |
Override with DOKI_DNS_LISTEN=IP:PORT.
Name Resolution
$ doki network create backend
$ doki run -d --name db --network backend postgres:alpine
$ doki run -d --name api --network backend my-api:latest
# From inside the api container:
$ doki exec api sh -c 'getent hosts db'
172.20.0.2 db.backend
# From the host (via the doki CLI):
$ doki network inspect backend
[
{
"Name": "backend",
"Id": "abc123",
"Containers": {
"db": {"EndpointID": "...", "IPv4Address": "172.20.0.2"},
"api": {"EndpointID": "...", "IPv4Address": "172.20.0.3"}
}
}
]
Aliases can be set with doki network connect --alias db postgres backend.
LRU Cache
The DNS server has a built-in LRU cache:
- 1024 entries
- 5-minute TTL per entry
- Re-registered on container restart
Key fixes
| File | Bug | Fix |
|---|---|---|
pkg/network/dns.go |
Busy-wait on SetReadDeadline |
ReadFromUDP blocks naturally |
pkg/common/resolv.go |
Stored :port in nameservers |
Stripped, appended :53 for dialling |
pkg/network/manager.go |
DNS not registered on container start | SetupNetwork calls AddEntry |
cmd/dokid/main.go |
Used :53 on Android (blocked) |
Uses :8053 on Android |
pkg/network/dns.go |
No AAAA, no PTR | Added both |
pkg/common/resolv.go |
ndots:5 caused retry loops | ndots:0 default |
pkg/network/dns.go |
UDP-only | TCP retry on TC bit (RFC 5966) |
pkg/network/manager.go |
Lost DNS on daemon restart | recoverContainers calls ReRegisterDNS |
CNI Plugins
CNI (Container Network Interface) is a spec for pluggable networking. Doki supports these plugins:
| Plugin | Purpose |
|---|---|
bridge |
Linux bridge (default) |
host-local |
Local IP allocation |
portmap |
Port mapping |
macvlan |
Direct host NIC access |
ipvlan |
L3 isolation |
dhcp |
DHCP-based IP allocation |
vlan |
802.1Q VLAN tagging |
CNI is enabled with DOKI_CNI=/path/to/cni/conf. The default bridge mode doesn't use CNI directly (faster, no plugin overhead).
Status: Plugin manager exists, not fully wired into the runtime. See Known Limitations.
Rootless Networking (pasta)
For users without root, Doki uses pasta (the "pasta" tool, successor to slirp4netns). Pasta:
- TCP/UDP connectivity without root or TAP devices
- ICS (Internet Connection Sharing) mode
- Built-in DHCP server for the container
- Bind mounts work normally
Usage:
# pasta is auto-detected in $PATH # or set DOKI_PASTA=/path/to/pasta doki run --rm --network bridge alpine ping -c 1 8.8.8.8
Pasta listens on the host's external interface and NATs traffic for the container. Performance is ~95% of native (vs 70% for slirp4netns).
IPv6
Enable on the default bridge:
{
"network": {
"ipv6": true
}
}
Or create an IPv6 network explicitly:
doki network create --ipv6 --subnet fd00::/64 ipv6-net
Doki assigns both v4 and v6 addresses when ipv6: true.
Veth Teardown
When a container is removed, its veth pair must be deleted to avoid leaking interfaces on the host. Doki tracks veth pairs:
// pkg/network/manager.go
type Endpoint struct {
// ...existing fields...
VethHost string // host-side interface name (e.g. "vethabc123")
VethPeer string // container-side interface name (e.g. "eth0")
}
teardownBridgeNetwork() now does:
// Delete both veth ends
exec.Command("ip", "link", "del", endpoint.VethHost).Run()
// (VethPeer goes away automatically with the pair)
// Then delete the bridge
exec.Command("ip", "link", "del", bridgeName).Run()
Without tracking: ip link would show dozens of veth* interfaces after running a few containers.
Security Considerations
| Concern | Mitigation |
|---|---|
| Container sniffing host traffic | Use --network bridge (default), not host |
| Container escaping via iptables manipulation | DOKI chain is namespaced, separate from system rules |
| DNS spoofing | DNS responses are bound to the container's IP |
| Port hijacking | First container to claim a host port wins; second fails with EADDRINUSE |
| ARP spoofing | Doki enables arp_ignore/arp_announce on veth interfaces |
Performance
| Mode | Throughput | Latency overhead |
|---|---|---|
bridge (rootful) |
95% native | <0.1ms |
bridge (rootless, pasta) |
90% native | ~0.2ms |
host |
100% native | 0ms |
none |
(no network) | n/a |
Source
pkg/network/manager.go— bridge, port forwarding, veth, teardownpkg/network/cni.go— CNI plugin manager, DOKI chainpkg/network/dns.go— internal DNS serverpkg/network/android_dns.go— Android DNS discoverypkg/network/rootless.go— pasta integrationpkg/common/resolv.go— resolv.conf parsing