Every device that wants to talk over a network needs an address so other devices know where to send data. That address is called an IP address.
Think of it like a house address. If someone wants to mail you a letter, they need your address. Same idea: if your phone wants to send data to Google, it needs to know Google's address, and Google needs to know your phone's address to send the reply back.
An IP address looks like this: 192.168.1.5
It is four numbers (0-255) separated by dots. Each number is called an octet.
A network is just a group of devices that can talk to each other directly.
Imagine a room with 5 laptops all connected to the same Wi-Fi router. Those 5 laptops are on the same network. They can send files to each other without going through the internet.
LAN = the network inside your home (or office, or in our case, inside the container world).
It is called "local" because it is physically nearby: your phone, laptop, smart TV, all connected to your home Wi-Fi router. They all live on the same LAN. They can talk to each other directly.
LAN addresses usually look like: - 192.168.x.x - 10.x.x.x - 172.16.x.x to 172.31.x.x
These are called private IP ranges. They are reserved for local networks and are never used on the public internet.
WAN = the network outside your home: the internet itself.
Your router has two sides: - The LAN side faces your devices at home - The WAN side faces your internet provider (ISP)
Your ISP gives your router one public IP address for the WAN side. Everything inside your home shares that one public IP to reach the internet.
[Your Phone]--+
[Your Laptop]-+--[Router]--[ISP]--[The Internet]
[Your TV]-----+
(LAN side) (WAN side)
A gateway is the device that connects two different networks together.
In your home, the router is the gateway. Your phone's IP is 192.168.1.5 (LAN). When your phone wants to reach Google at 142.250.80.46 (WAN/internet), it does not know how to get there directly. So it sends the data to the gateway (router), and the router figures out how to forward it to the internet.
Rule: Every device on a LAN is configured with a "default gateway" - the address they send all traffic to when they do not know where else to send it.
There are actually two kinds of addresses in networking:
| Type | Looks like | Purpose |
|---|---|---|
| IP address | 192.168.1.5 | Logical address - used for routing across networks |
| MAC address | a4:c3:f0:12:34:56 | Physical address - used for delivery on the same network |
Think of it this way: - The IP address is the city and street - used to navigate across the country - The MAC address is the apartment number - used once you arrive at the building
When your laptop sends a packet to your router, it uses the router's MAC address (because they are on the same LAN). The router then uses IP addresses to figure out where to send it next.
Data traveling over a network is broken into small chunks called packets. Each packet contains: - Where it came from (source IP) - Where it is going (destination IP) - A small piece of the actual data
The network reassembles all the packets at the destination.
Every device needs an IP address to join a network. But you cannot have two devices with the same IP - that would be like two houses with the same postal address. Mail would get lost.
You could manually assign a unique IP to every device, but that is painful. What if you have 50 devices?
DHCP = Dynamic Host Configuration Protocol
It is a system where one device (the DHCP server) automatically hands out IP addresses to every new device that joins the network.
The conversation goes like this:
New Device: "Hello? Anyone there? I just joined this network and I need an IP address."
DHCP Server: "I heard you. Here, take 192.168.1.42. Also, your gateway is 192.168.1.1,
and for DNS use 1.1.1.1. Your lease lasts 24 hours."
New Device: "Got it, thanks!"
The new device now has everything it needs to work on the network: - Its own IP address - The gateway address (so it knows where to send traffic) - The DNS address (explained next)
In your home, the router runs the DHCP server. It hands out IPs to every device that connects.
In Droidspaces NAT mode, Droidspaces itself runs a mini DHCP server that hands the container its IP (in the 172.28.x.x range). The IP is deterministic: it is derived from the container's name, persisted to its config file, and offered again on every boot, so the same container always keeps the same address across restarts.
IP addresses are hard to remember. Nobody types 142.250.80.46 when they want to visit Google. They type google.com.
But computers only understand IP addresses. So there needs to be a system that converts human-readable names to IP addresses.
DNS = Domain Name System
It is basically the internet's phone book. You give it a name (google.com), it gives you back an IP address (142.250.80.46).
The conversation:
Your Browser: "What is the IP address of google.com?"
DNS Server: "It is 142.250.80.46"
Your Browser: "Thanks." [now connects to 142.250.80.46]
Every device is configured with a DNS server address. In most home networks, the router is the DNS server (it forwards your questions to your ISP's DNS or a public one like 1.1.1.1).
In Droidspaces NAT mode, Droidspaces writes a resolv.conf file inside the container pointing to a DNS server (default 1.1.1.1 and 8.8.8.8, or whatever you pass via --dns). The same DNS servers are also advertised inside the DHCP lease itself.
Your ISP gives you one public IP address. But you have 10 devices at home. How do all 10 devices use the internet at the same time?
NAT = Network Address Translation
Your router keeps a secret table. When a device inside your LAN sends a packet to the internet, the router: 1. Rewrites the source IP from the device's private IP (192.168.1.5) to the router's public IP 2. Remembers which device sent it 3. When the reply comes back from the internet, rewrites the destination back to the device's private IP and forwards it
From the internet's perspective, all your home devices appear to be one device: the router.
[Laptop: 192.168.1.5] --sends packet--> [Router]
|
| rewrites source to public IP
v
[Internet]
|
| reply comes back
v
[Router]
|
| rewrites destination back to 192.168.1.5
v
[Laptop: 192.168.1.5]
In Droidspaces NAT mode, Droidspaces acts exactly like your home router - but for containers:
172.28.x.x)
MASQUERADE rules (that is the Linux name for the NAT target), plus FORWARD-accept and MSS-clamp rules so traffic actually flows
A NAT container has to know which of Android's real interfaces currently has internet, so it can MASQUERADE through it. Phones make this hard: the active network hops between Wi-Fi, mobile data, USB-ethernet and VPN tunnels, and the interface names themselves are unstable (the mobile-data interface might be rmnet0 one minute and rmnet8 after a reconnect).
By default Droidspaces handles all of this for you - there is nothing to configure:
netd installs for the active default network; on desktop Linux it is the main routing table's default route.
v4-rmnet... interfaces phones synthesise on IPv6-only mobile networks) are picked up automatically.
This automatic mode is the right choice for the vast majority of users. The rest of this section is only relevant if you want to override it.
--upstreamSometimes you do not want the container to follow whatever network the host is using. You want to force its internet out through one specific interface and keep it there. That is what --upstream does.
When you pass --upstream, automatic detection is switched off entirely. The interface(s) you list become the only candidates the container will ever use for WAN - it never hops to whatever the host marks as its active default network.
# Force the container's internet out through Wi-Fi, always
droidspaces --name=box --rootfs=/data/box --net=nat --upstream=wlan0 start
You can list multiple interfaces, comma-separated, and use wildcards (*, ?):
droidspaces --name=box --rootfs=/data/box --net=nat --upstream=wlan0,rmnet* start
The list is priority-ordered. The route monitor walks it top to bottom and uses the first interface that is currently up and actually has internet. So wlan0,rmnet* means "prefer Wi-Fi; if Wi-Fi is down, fall back to mobile data" - and when Wi-Fi returns, it switches back. The failover stays strictly inside your list; it never falls back to an interface you did not list. This is the key difference from auto mode - the WAN is yours to decide, not the host's.
If a pinned interface is missing when the container starts, or disappears mid-session and later reappears, that is handled too: the container simply has no WAN until one of your pinned interfaces is up, then it wires up automatically.
Why wildcards matter on mobile data: Android does not give the mobile-data interface a stable number - it might be
rmnet0,rmnet8,rmnet_data2, and the number can change across reconnects. Pinning a literalrmnet0will break the next time it comes up as something else. Pinrmnet*instead and it keeps working.
--upstream1. Route the container's WAN through an Android VPN (tun0)
Connect a VPN on the phone itself - ProtonVPN, WireGuard, OpenVPN, any app that creates a tun0 interface. Then pin the container to it:
droidspaces --name=box --rootfs=/data/box --net=nat --upstream=tun0 start
Now all of the container's traffic goes out through the VPN tunnel, and only the tunnel. If the VPN drops, tun0 disappears and the container loses internet instead of leaking out over your real connection - a simple killswitch, for free.
2. Container on mobile data while the phone stays on Wi-Fi
Android can keep the cellular radio up even while you are on Wi-Fi. Enable "Mobile data always active" in Developer Options, connect to Wi-Fi, then turn mobile data on. Both networks are now live at once. Pin the container to the mobile-data interface:
droidspaces --name=box --rootfs=/data/box --net=nat --upstream=rmnet* start
The container's traffic goes out over mobile data while the rest of the phone keeps using Wi-Fi. Handy for testing from a different IP/network, putting a container's bandwidth onto cellular, or simply running something on a separate connection from everything else on the phone.
Now we go one level deeper, into how Linux actually connects containers to each other.
A network bridge works like a network switch. A physical network switch is a box you plug multiple ethernet cables into; all devices connected to it can talk to each other.
A Linux bridge is a virtual switch, entirely in software. You can create it with a command, and then "plug" virtual network interfaces into it.
Physical world: Linux world:
+--------------+ +--------------+
| Switch | | Bridge | (software, no physical box)
| port1 port2 | | port1 port2 |
+--+------+---+ +--+------+---+
| | | |
[PC1] [PC2] [veth1] [veth2] (virtual cables)
veth = virtual ethernet
A veth pair is a pair of virtual network interfaces that are connected to each other like a pipe. Anything you send into one end comes out the other end.
Think of it as a virtual ethernet cable with two plugs. You put one plug inside a container, and the other plug stays on the host (or goes into a bridge).
[Container netns] [Host netns]
eth0 --------------------- ds-veth0
(plug inside container) (plug on host side)
In Droidspaces NAT mode:
[Container netns]
eth0 (e.g. 172.28.137.42)
|
| veth pair (virtual cable)
|
[Host side]
ds-v ---- ds-br0 (bridge, has IP 172.28.0.1)
|
iptables MASQUERADE
|
wlan0 / rmnet0
(Android's real network)
The bridge ds-br0 owns the gateway IP 172.28.0.1, which every NAT container uses as its default gateway. The veth pair is named after the container's init process ID: the host side is ds-v, and the container side starts life as ds-p before being renamed to eth0 inside the container.
Droidspaces runs a small per-container DHCP server listening on the container's host-side veth. The whole 172.28.0.0/16 subnet belongs to Droidspaces (the 172.28.0.x row is reserved for the gateway itself, so containers always land in 172.28.1.x through 172.28.254.x), and everything from it is NATted out through Android's real interface.
Linux has a feature called namespaces that lets you create isolated views of system resources.
A network namespace is an isolated copy of the entire networking stack. It has its own: - Network interfaces - Routing table - iptables rules - Everything networking-related
When Droidspaces starts a container, it creates a new network namespace for it. The container lives in that namespace. It cannot see the host's network interfaces at all - only what Droidspaces explicitly puts inside its namespace.
The veth pair is the tunnel between the host's namespace and the container's namespace: - One end of the veth goes inside the container's network namespace (appears as eth0) - The other end stays in the host's network namespace (Droidspaces connects it to a bridge)
OpenWRT is a Linux distribution designed specifically for routers. Normally it runs on physical router hardware, but it can also run on a regular Linux system or inside a container.
When OpenWRT is running, it provides: - netifd - network interface daemon (manages network interfaces, DHCP client/server, etc.) - dnsmasq - DNS and DHCP server - firewall3 or nftables - firewall - LuCI - web UI for configuration - Everything a real router does, in software
This means you can run OpenWRT inside a Droidspaces container, and it will behave exactly like a real router: managing networks, handing out DHCP leases, doing DNS, applying firewall rules, routing VPN traffic, etc.
In NAT mode, Droidspaces is the router. It does everything. This is fine for most cases.
But what if you want OpenWRT to be the router for other containers? You want OpenWRT's firewall rules, OpenWRT's DHCP, OpenWRT's VPN routing, and you want other containers (like a Kali Linux container) to be on OpenWRT's LAN, getting everything from OpenWRT.
The problem: if Droidspaces also tries to install NAT, DHCP, and DNS for those containers, it will conflict with what OpenWRT is trying to do. Two DHCP servers fighting over who gives the IP address. Two firewalls applying contradictory rules.
Gateway mode solves this. Droidspaces steps back. It only does the L2 plumbing (the virtual cables and switch), and lets OpenWRT own all the policy: DHCP, DNS, firewall, routing.
Android host kernel
|
+-- wlan0 (Android's real Wi-Fi - WAN)
|
+-- [OpenWRT container - net=nat mode]
| netns: owns eth0 (WAN, gets NAT from Droidspaces)
| eth1 (LAN side - plugged into ds-lan bridge by gateway mode)
| Runs: dnsmasq, netifd, firewall, VPN
|
+-- ds-lan (host bridge - NO IP address, just a switch)
| |
| +-- ds-g[hash] (veth host-side, connected to OpenWRT's netns as eth1)
| +-- ds-c[pid] (veth host-side, connected to Kali's netns as eth0)
|
+-- [Kali container - net=gateway mode]
netns: owns eth0 (LAN side - plugged into ds-lan bridge)
Gets DHCP from OpenWRT's dnsmasq
Routing decisions made by OpenWRT
Firewall rules applied by OpenWRT
Step 1 - You start OpenWRT first (in NAT mode)
droidspaces --name=openwrt --rootfs=/data/openwrt --net=nat start
OpenWRT boots. It has: - eth0 on the WAN side (Droidspaces manages NAT for this) - No LAN side yet - OpenWRT is waiting for one
Step 2 - You start Kali (in gateway mode)
droidspaces --name=kali --rootfs=/data/kali --net=gateway --gateway=openwrt start
Droidspaces does the following (only plumbing, no policy):
ds-lan on the host with no IP address on it
bridge-nf-call-iptables so Android's host firewall does NOT intercept traffic on this bridge, keeping OpenWRT's firewall as the only authority
eth1), the other end plugs into the ds-lan bridge
eth0), the other end plugs into the ds-lan bridge
Step 3 - OpenWRT takes over
OpenWRT's netifd detects that eth1 appeared. It configures it as the LAN interface. OpenWRT's dnsmasq starts answering DHCP requests on eth1.
Kali's eth0 sends a DHCP request and OpenWRT's dnsmasq replies with: - IP address: 192.168.1.100 (or whatever OpenWRT's DHCP range is) - Gateway: 192.168.1.1 (OpenWRT itself) - DNS: 192.168.1.1 (OpenWRT's dnsmasq)
Kali is now fully configured, with OpenWRT as its router.
Step 4 - Traffic flows through OpenWRT
When Kali tries to reach the internet:
Kali eth0 --> ds-lan bridge --> OpenWRT eth1
|
OpenWRT firewall rules applied here
|
OpenWRT routes to eth0 (WAN)
|
Droidspaces NAT (eth0 -> wlan0)
|
Android wlan0 --> Internet
OpenWRT's firewall sees all of Kali's traffic and can apply any rules: block certain sites, redirect through VPN, shape bandwidth, log connections - exactly like a real router would.
The gateway veth is "lazily attached." This means:
eth1
eth1 only appears inside OpenWRT when the first gateway-mode container starts
eth0), and its LAN cable (eth1) is plugged in later, on demand
This mimics how you might physically plug a cable into a router's LAN port after the router is already running.
In NAT mode, Droidspaces writes /etc/resolv.conf inside the container, pointing to 1.1.1.1 or 8.8.8.8.
In gateway mode, Droidspaces does NOT write a static resolv.conf (unless you explicitly pass --dns). This is because OpenWRT's dnsmasq hands the DNS server address to the container via the DHCP lease. If Droidspaces also wrote a resolv.conf, it would conflict with what dnsmasq is providing - the container would use the wrong DNS and bypass OpenWRT's DNS filtering/caching entirely.
How this is wired depends on the client's init system:
/etc/resolv.conf is symlinked to /run/systemd/resolve/resolv.conf, which systemd-resolved populates from the DHCP lease.
/etc/resolv.conf entirely alone, so the container's own DHCP client (udhcpc/dhclient) writes the gateway-supplied nameserver from the lease. (Earlier builds wrote a hardcoded 1.1.1.1/8.8.8.8 here, which silently bypassed the gateway's DNS - that is fixed.) If a minimal rootfs ships no DHCP resolv.conf hook, pass --dns to set one explicitly.
The bridge ds-lan carries traffic between OpenWRT and Kali. By default, Linux can pass bridged traffic through the host's iptables. This would mean Android's iptables rules (which might drop or NAT things unexpectedly) would interfere with traffic that OpenWRT is supposed to be managing.
Setting it to 0 tells Linux: "do not run iptables on bridged traffic." This keeps OpenWRT's firewall as the only firewall that sees this traffic, which is exactly what we want.
All wiring for a gateway-mode client is done from the host side by a single function, gateway_wire_client(): it ensures the bridge and the gateway-side cable, creates the client's app veth, and moves+renames the peer into the client's namespace as eth0 (pinned MAC, brought up) — the client's own boot code only brings up lo. Because the host owns every step, the same function wires a client whether it is just starting or already running.
This gives a deliberately simple rule keyed on gateway liveness:
gateway_wire_client).
eth0) and just boots. The work is deferred entirely to the gateway.
Healing is therefore driven by the gateway itself, not by the clients. On every boot cycle the gateway container's monitor calls ds_net_rewire_gateway_clients(): it scans the running containers, finds the ones that delegate to this gateway, and runs gateway_wire_client for each — establishing the gateway-side eth1 cable and every client's eth0 into the gateway's current namespace. So when the gateway starts or reboots, every running client is (re)wired with no client restart required.
Skipping all client wiring while the gateway is down (rather than half-wiring a bridge and a danging veth) also closes a race: a client started before its gateway can have its gateway/LAN settings (--gateway-net, --host-bridge, …) edited before the gateway comes up, and the gateway then wires every client from each client's current config — never a stale one.
There is exactly one actor (the gateway) doing the wiring, so there is nothing to poll and no thundering herd. Wiring is serialised per segment (an advisory file lock) to keep concurrent client starts and the gateway's re-wire from racing. Both eth1 (gateway side) and each eth0 (client side) keep a stable MAC and are moved+renamed into their namespace in a single atomic step, so the container's own netifd/DHCP sees one persistent device rather than re-initialising a churning one.
Cleanup in gateway mode is deliberately minimal, matching the "plumbing only" philosophy:
ds-c prefix, distinct from NAT's ds-v). The bridge and the gateway's eth1 stay up, so other clients on the segment are untouched.
eth1 carrier and occasionally make netifd report "device initialization failed"; an idle IP-less bridge is harmless and the next client reuses it.
Only one flag is mandatory when using --net=gateway:
--gateway=
If you omit it, Droidspaces will print an error and refuse to start. Everything else has a working default:
| Flag | Default | What it controls |
|---|---|---|
--gateway=NAME | (none - required) | Which running container is the router |
--gateway-net=NAME | lan | The LAN segment name - see below |
--gateway-iface=IFACE | eth1 | Interface name inside the gateway container |
--gateway-bridge=BR | ds-{gateway-net} | Override the host bridge name entirely |
So the minimal valid command is:
droidspaces --name=client --net=gateway --gateway=openwrt start
That is identical to spelling out all defaults explicitly:
droidspaces --name=client --net=gateway --gateway=openwrt \
--gateway-net=lan \
--gateway-iface=eth1 \
start
This flag controls two things at once, both derived from the same name.
1. It names the host bridge.
The bridge Droidspaces creates on the host is named ds-{NAME}:
--gateway-net=lan -> host bridge: ds-lan
--gateway-net=vpn -> host bridge: ds-vpn
--gateway-net=iot -> host bridge: ds-iot
2. It is the segment identifier - which bridge clients land on.
The veth names for the gateway's LAN side are generated by hashing the string {gateway_container}:{gateway_net} together. The same hash = the same veth = the same bridge segment. This means multiple client containers that share the same --gateway and --gateway-net all end up on the same bridge, and all get DHCP from the same OpenWRT interface.
This is the real power of --gateway-net: running multiple isolated LAN segments through the same gateway container.
# These two land on ds-lan - they see each other, OpenWRT routes them as one LAN
droidspaces --name=kali --net=gateway --gateway=openwrt --gateway-net=lan start
droidspaces --name=ubuntu --net=gateway --gateway=openwrt --gateway-net=lan start
# This one lands on ds-vpn - a completely separate bridge
# OpenWRT can apply different firewall/VPN rules to this segment
droidspaces --name=torbox --net=gateway --gateway=openwrt --gateway-net=vpn start
Inside OpenWRT, the lan clients come in on eth1 and the vpn clients come in on eth2 (each segment gets its own veth, because the hash of openwrt:lan and openwrt:vpn are different).
This controls what the LAN interface is called inside the gateway container's network namespace.
When Droidspaces creates the gateway veth for a segment, it moves one end into OpenWRT's netns and renames it from its raw hash name (ds-hXXXXXXXX) to whatever you pass here (default eth1).
Why does this matter? OpenWRT's configuration is built around interface names. If your OpenWRT /etc/config/network says:
config interface 'lan'
option device 'eth1'
...then the interface that appears inside OpenWRT must be named eth1, or OpenWRT will not recognize it as its LAN and will not serve DHCP on it. --gateway-iface=eth1 makes that happen.
For a second segment you would pass --gateway-iface=eth2 so OpenWRT sees it as a separate interface and you can add a second UCI network block for it.
Important detail: --gateway-iface only has any effect when the gateway veth for that segment is being created for the first time - which is when the very first client container on that segment starts. The gateway veth is shared by all clients on the same --gateway-net; it is created once and reused. Every client after the first one skips the gateway veth creation entirely and just wires its own app veth into the existing bridge.
This means if you start two containers on --gateway-net=lan and both pass --gateway-iface=eth1, it works perfectly fine: the first container creates the veth and renames it eth1, the second container sees the veth already exists and does nothing with --gateway-iface at all.
The problem only happens when you use two different --gateway-net segments but the same --gateway-iface:
# segment 1 - creates eth1 inside OpenWRT
droidspaces --name=kali --net=gateway --gateway=openwrt --gateway-net=lan --gateway-iface=eth1 start
# segment 2 - WRONG: also tries to create eth1 inside OpenWRT
droidspaces --name=torbox --net=gateway --gateway=openwrt --gateway-net=vpn --gateway-iface=eth1 start
When the second command runs, Droidspaces tries to move a new veth peer into OpenWRT and rename it eth1. But eth1 already exists inside OpenWRT from the first segment. Instead of failing loudly, the code detects the conflict and just brings the existing eth1 up again, leaving the new veth peer with its raw hash name (ds-hYYYYYYYY) inside OpenWRT. OpenWRT has no config for ds-hYYYYYYYY and silently ignores it. The vpn segment gets no gateway-side interface: no DHCP, no routing, containers on it are effectively isolated.
The rule: every --gateway-net segment must have a unique --gateway-iface name.
# Correct: two segments, two interface names
--gateway-net=lan --gateway-iface=eth1 -> eth1 inside OpenWRT (LAN segment)
--gateway-net=vpn --gateway-iface=eth2 -> eth2 inside OpenWRT (VPN segment)
Droidspaces enforces a few rules at startup and refuses to boot if they are violated:
--gateway must name a different container)
IFNAMSIZ limit) and may only contain letters, digits, _, and -
CONFIG_NET_NS), veth pairs (CONFIG_VETH), and bridges (CONFIG_BRIDGE). Droidspaces probes for all three before starting and exits with a fatal error if any is missing
Two more things to know:
--port only makes sense in NAT mode. In gateway mode it is ignored with a warning - port forwarding and uplink selection are the gateway container's job now.
--gateway-net, the name is sanitized (only letters, digits, _, - survive) and truncated to 9 characters, producing ds- plus at most 9 characters. If you need an exact bridge name, set it explicitly with --gateway-bridge.
| Feature | NAT Mode | Host Mode | None Mode | Gateway Mode |
|---|---|---|---|---|
| Who assigns IPs? | Droidspaces DHCP | Android (shared) | Nobody (loopback only) | OpenWRT dnsmasq |
| Who does NAT? | Droidspaces iptables | Android | N/A | OpenWRT (via Droidspaces NAT on OpenWRT's WAN) |
| Who manages firewall? | Droidspaces | Android | N/A | OpenWRT |
| Who manages DNS? | Droidspaces | Android | Nobody | OpenWRT dnsmasq |
| Container isolated from host network? | Yes | No | Yes | Yes |
| Internet access? | Yes | Yes | No | Yes (via gateway container) |
| Needs a second container to function? | No | No | No | Yes (the gateway container) |
| Good for | Simple internet access | Maximum performance, zero overhead | Offline / sandboxed workloads | Router appliance, VPN gateway, segmented LANs |
Run OpenWRT with a WireGuard or OpenVPN client. Configure OpenWRT's firewall to drop all traffic that does not go through the VPN tunnel. Any container using gateway mode will be unable to leak traffic outside the VPN - OpenWRT enforces it at the bridge level, not inside the individual containers.
Use --gateway-net to create separate segments on the same OpenWRT. Containers on --gateway-net=lan cannot reach containers on --gateway-net=vpn unless OpenWRT explicitly routes between them. You get VLAN-style isolation with a single gateway container.
Run OpenWRT with tcpdump or nftables logging enabled. Every packet from every gateway-mode container flows through OpenWRT, so you get a single chokepoint to observe all network activity across multiple containers at once.
Run OpenWRT with a dnsmasq blocklist (or with Adblock installed via opkg). Every container on the gateway LAN gets filtered DNS without touching each container individually.
OpenWRT's tc (traffic control) and sqm-scripts can shape bandwidth per container, since OpenWRT sees each container as a separate MAC address arriving on its LAN interface.
| Term | One-line definition |
|---|---|
| IP address | The numerical address of a device on a network (e.g. 192.168.1.5) |
| MAC address | The hardware address of a network interface, used for delivery within the same network |
| LAN | Local network - devices near each other that can talk directly |
| WAN | Wide network - the internet, outside your local network |
| Gateway | A device that connects two networks and routes traffic between them |
| DHCP | Protocol for automatically assigning IP addresses to devices |
| DNS | System that converts human-readable names (google.com) to IP addresses |
| NAT | Technique for sharing one public IP across many private-IP devices |
| Bridge | A virtual (or physical) switch that connects multiple network interfaces |
| veth pair | A pair of virtual network interfaces connected like a pipe - what goes in one end comes out the other |
| Network namespace | An isolated copy of the Linux networking stack - containers live in their own namespace |
| OpenWRT | A Linux distro designed to run as a router/gateway - runs dnsmasq, netifd, firewall |
| netifd | OpenWRT's network interface daemon - manages interfaces and DHCP |
| dnsmasq | Lightweight DHCP and DNS server used by OpenWRT |
| MASQUERADE | The Linux iptables rule that implements NAT (rewrites source IPs) |
| Delegated LAN | The bridge network Droidspaces creates in gateway mode - policy owned by the gateway container, not Droidspaces |
| Segment | One isolated LAN identified by --gateway-net - each segment gets its own bridge and its own interface inside the gateway container |
| Lazy attachment | The gateway's LAN-side veth is only created when the first client container starts, not when the gateway container starts |
© 2026 Droidspaces · GPLv3 · by ravindu644 and contributors