Android Networking
A working Docker bridge does not guarantee working traffic on Android. netd policy routing and different iptables paths can break outbound traffic or published ports. This page follows the diagnosis used in the verified reference setup.
Symptoms
container -> docker bridge ARP succeeds
container -> Internet fails
or
container is healthy
host :3000 is LISTENING
LAN connection resets / times out
Android policy routing
Android has many ip rule entries managed by netd, and traffic does not always naturally fall through to the main table as it would on a conventional Linux host. Some devices need a rule that explicitly sends traffic arriving from the Docker bridge to lookup main.
ip rule
ip route show table main
ip route get 1.1.1.1
echo 1 > /proc/sys/net/ipv4/ip_forwardDo not flush existing netd rules. If an additional rule is required, reserve your own priority range and match only traffic originating from the Docker bridge. Choose a priority that does not conflict with the current output of ip rule.
ip -br link
ip -br addr
docker network inspect bridge --format '{{json .IPAM.Config}}'
ip route get 1.1.1.1
ip route show table all | head -n 120
legacy vs nft
Docker inside the Ubuntu chroot may create nft-based rules while the Android-side packet path actually traverses legacy iptables. In that situation published ports can fail. When you have confirmed that path, place dedicated chains in Android-side legacy PREROUTING / FORWARD and perform DNAT there.
iptables --version
iptables -t nat -S
iptables -S FORWARD
command -v iptables-legacy && iptables-legacy --version
command -v iptables-nft && iptables-nft --version
command -v nft && nft list ruleset | head -n 120iptables --version alone cannot prove which rule set real packets traverse in the Android kernel. Check packet counters on both Docker-created rules and Android-side chains.
Example dedicated chains
filter: VYDOCKER_IN / VYDOCKER_OUT / VYDOCKER_FWD
nat: VYDOCKER_PRE / VYDOCKER_POST
INPUT -> VYDOCKER_IN
OUTPUT -> VYDOCKER_OUT
FORWARD -> VYDOCKER_FWD
PREROUTING -> VYDOCKER_PRE
POSTROUTING -> VYDOCKER_POSTThe chain names are arbitrary. Do not flush Android/netd chains directly. Keep your rules isolated so that reapplying the configuration only requires flushing and rebuilding chains you own.
bridge → WAN
Interface names and subnets depend on the device, connection type, and Docker network. Discover the real values first.
WAN_IF=$(ip route get 1.1.1.1 | awk '/dev/ {for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}')
DOCKER_SUBNET=$(docker network inspect bridge --format '{{(index .IPAM.Config 0).Subnet}}')
echo "WAN=$WAN_IF subnet=$DOCKER_SUBNET"Example after confirming that the Android packet path uses legacy iptables:
iptables -A VYDOCKER_FWD -i docker0 -o "$WAN_IF" -s "$DOCKER_SUBNET" -j ACCEPT
iptables -A VYDOCKER_FWD -i "$WAN_IF" -o docker0 -d "$DOCKER_SUBNET" \
-m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
iptables -t nat -A VYDOCKER_POST -s "$DOCKER_SUBNET" -o "$WAN_IF" -j MASQUERADEIf docker run --rm alpine ping -c 1 1.1.1.1 works at this point, L3/NAT is working. Test DNS separately with tools such as wget or nslookup.
LAN published port
If curl http://127.0.0.1:3000/healthz works on the host but only LAN clients fail, investigate the host's published-port path rather than Vyline. Re-discover the target container IP and bridge instead of hard-coding them.
CID=$(docker ps -qf 'name=vyline')
CONTAINER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$CID")
NETWORK_ID=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.NetworkID}}{{end}}' "$CID")
BRIDGE_IF="br-$(printf '%s' "$NETWORK_ID" | cut -c1-12)"
ip -br addr
echo "container=$CONTAINER_IP bridge=$BRIDGE_IF"After identifying the interface that receives LAN traffic as LAN_IF:
LAN_IF=wlan0 # example; replace with the actual interface
iptables -t nat -A VYDOCKER_PRE -i "$LAN_IF" -p tcp --dport 3000 \
-j DNAT --to-destination "$CONTAINER_IP:3000"
iptables -A VYDOCKER_FWD -i "$LAN_IF" -o "$BRIDGE_IF" \
-p tcp -d "$CONTAINER_IP" --dport 3000 -j ACCEPTWi-Fi, USB tethering, mobile data, and VPN connections can use different input and default-route interfaces. wlan0 is only an example.
Following container recreation
If Compose recreation changes the container IP or bridge name, fixed DNAT/FORWARD rules become stale. On devices that need these rules, re-enumerate the Docker network, container, and LAN/WAN interfaces, then resync only your dedicated chains.
loop
docker network inspect -> bridge/subnet
docker inspect -> container IP / published ports
rediscover LAN/WAN interface/gateway
rebuild ip rules only in your reserved priority range
rebuild dedicated iptables chains
update only when state changesPolling every few seconds is simple but not mandatory. Docker events, network-change hooks, or rebuilding at boot may be better depending on the device.
Optional remote-access path
Cloudflare Tunnel is not required to start Vyline. If remote access is needed, placing cloudflared on the same Docker network as Vyline and using http://vyline:3000 as the origin bypasses the Android host's LAN-side published-port path. There is no reason to add it for LAN-only use.
Final checks
docker run --rm alpine ping -c 1 1.1.1.1
docker run --rm alpine sh -c 'wget -qO- https://example.com | head'
curl -v http://127.0.0.1:3000/healthz
ss -lnt | grep ':3000'
iptables -nvL FORWARD
iptables -t nat -nvL PREROUTINGVerify “container → Internet”, “host → Vyline”, and “LAN → host:3000” separately. Trying to fix all three at once makes it difficult to identify which layer is actually broken.