Vyline DOCS

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_forward

Do 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 120

iptables --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_POST

The 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 MASQUERADE

If 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 ACCEPT

Wi-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 changes

Polling 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 PREROUTING

Verify “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.

Search by page, setting, or command