Networking & port forwarding
The Alpine guest has full outbound internet access from first boot. Inbound access - SSH, VNC, container services - works through port forwarding, with a few rules injected automatically and the rest configured in Settings.
Guest network: SLIRP user-mode
On the QEMU backend the guest network is provided by SLIRP, a userspace TCP/IP stack embedded inside QEMU. No root, no kernel TUN/TAP device, and no host-side network interface are involved. From inside the guest the network looks like a small NAT:
| Address | Role |
|---|---|
10.0.2.15/24 | Guest IP (eth0) |
10.0.2.2 | Default gateway (SLIRP host) |
10.0.2.3 | Built-in DNS resolver |
8.8.8.8, 1.1.1.1 | Fallback DNS (in /etc/resolv.conf) |
IPv6 is disabled on the QEMU netdev (ipv6=off). Outbound TCP and UDP both work. The podroid-network OpenRC service brings up eth0 with these addresses at boot.
On the AVF backend the guest receives its IP from Android's built-in tethering via DHCP (udhcpc), rather than a static SLIRP address. The default route and DNS are assigned by the Android network stack. Inbound connectivity still works through port forwarding as described below, but the transport is vsock rather than SLIRP hostfwd.
Implicit port forwards
Three port forwards are injected automatically every time the VM starts. They never appear in the port-forward UI and cannot be removed from there:
| Service | Host port | Guest port | Protocol | When active |
|---|---|---|---|---|
| SSH (dropbear) | 9922 |
22 |
TCP | When SSH is enabled in Settings |
| X11 / VNC (Xvnc) | 5900 |
5900 |
TCP | Always (backs the in-app screen viewer) |
| PulseAudio | 4713 |
4713 |
TCP | Always (backs the in-app audio stream) |
The SSH forward is conditional: if you turn SSH off in Settings, the 9922→22 rule is not added and dropbear does not start inside the guest. The VNC and PulseAudio forwards are unconditional because the in-app X11 viewer depends on them.
Adding your own rules
Open Settings → Port forwards → Add. Enter a host port (on the Android device), a VM port (inside the guest), and pick TCP, UDP, or Both. On the QEMU backend rules apply live via the QMP protocol - no VM restart is needed. Rules are persisted across reboots.
A typical setup for a container service listening on guest port 8080:
# Inside the guest: start your service on port 8080
podman run -d -p 8080:80 nginx
# In Settings -> Port forwards -> Add:
# Host port: 8080
# VM port: 8080
# Protocol: TCP
#
# Then from your laptop on the same Wi-Fi:
curl http://<phone-ip>:8080
Talking back to Android from the guest
Several command-line tools ship inside the VM so scripts and shell sessions can reach the Android side directly, without the in-app UI. They all work on the QEMU and AVF backends.
podroid-forward - manage port forwards from the shell
# forward phone :8888 -> guest :80
podroid-forward add 8888 80 tcp
# shorthand for "add ... tcp"
podroid-forward 8888 80
# list current rules
podroid-forward list
# drop a rule
podroid-forward remove 8888 tcp
Rules created this way are identical to ones added in Settings: they apply live, persist across reboots, and appear in the Settings list. Both TCP and UDP work on either backend.
podroid-notify - post an Android notification
# simplest form
podroid-notify "build finished"
# with a title and priority (low | normal | high)
podroid-notify --title Podroid --priority high "container crashed"
# reuse --id to update the same notification in place
podroid-notify --id 7 "progress 50%"
Handy for long builds, downloads, cron jobs and container health alerts. Tapping the notification opens Podroid. Requires the standard Android notification permission, requested at first run.
podroid-open - open a URL on the phone
# open a link in the phone's default browser
podroid-open https://github.com/ExTV/Podroid
Hands the URL to Android's ACTION_VIEW intent, so it opens in whatever app handles it on the phone. Only http and https URLs are accepted.
podroid-power - control the VM from inside the guest
# stop the VM cleanly - the app returns to its stopped state
podroid-power stop
# restart the VM
podroid-power restart
# print the current VM state, e.g. Running
podroid-power status
A clean lifecycle hook routed through the app, unlike an in-guest poweroff (which the app would treat as an unexpected exit). Useful for scripts that need to cycle the VM, or to shut it down from an SSH session.
podroid-server - server (headless) mode
# turn server mode on - the screen goes black at minimum brightness
podroid-server on
# turn it back off
podroid-server off
# query state (on | off)
podroid-server status
Server mode keeps the VM running behind a pure-black, minimum-brightness full-screen overlay - for leaving the phone on a headless workload with minimal OLED draw and burn-in. The screen stays on (so the system does not kill the VM) but shows nothing. Exit by holding the screen for three seconds, or run podroid-server off. The same toggle lives in the terminal's top bar. podroid-headless is an alias for podroid-server.
A small guest daemon (podroid-hostd) relays each request to the Android side over the same control transport the app already uses - a virtio-console port on QEMU, vsock on AVF - which then performs the requested action: post a notification, write a port-forward rule, open a URL, or control the VM. Any process that can reach the guest socket can use these, so they work from a container too if you bind-mount /run/podroid-host.sock into it.
LAN reachability
All port forward listeners bind 0.0.0.0, not 127.0.0.1. This means any device on the same Wi-Fi network can reach the phone by its local IP address. The phone's current IP is shown in Settings.
# SSH from a laptop on the same Wi-Fi
$ ssh root@<phone-ip> -p 9922
# password: podroid
# VNC from any viewer
$ vncviewer <phone-ip>:5900
# HTTP service forwarded to host port 8080
$ curl http://<phone-ip>:8080
The privileged-port limit
Android apps run as unprivileged Linux users and do not hold CAP_NET_BIND_SERVICE. The Linux kernel refuses to bind a TCP or UDP socket to any port below 1024 for a process without that capability. This is a kernel-level rule, not a Podroid restriction, and there is no in-app workaround without root.
Consequences:
- SSH is exposed on host port 9922, not 22. Port 22 is below 1024; 9922 is not.
- You cannot listen on host ports
:80or:443directly. - The Add-rule dialog accepts any value from 1 to 65535, but a host port below 1024 will fail to bind at VM start time with a bind error in the logs. The rule itself is accepted - the failure happens when QEMU or the vsock forwarder tries to open the socket.
Forward to a high host port instead of the standard one: map host 8080 to guest 80, and host 8443 to guest 443. A daemon running as guest-root can still listen on 80 or 443. A rootless Podman or Docker container is the exception: its published port is also subject to the ≥1024 limit inside the guest, so publish to a high guest port too - e.g. a Pi-hole DNS container on guest 5353/udp, forwarded to host 5353, with LAN clients pointed at <phone-ip>:5353.
# Forward a web server on guest :80 to a high host port
# Settings -> Port forwards -> Add: 8080 -> 80 TCP
# Then reach it from outside:
$ curl http://<phone-ip>:8080
Port forwarding on AVF
On the AVF backend, port forwarding is implemented over vsock rather than SLIRP hostfwd. Each rule creates a listener on 0.0.0.0:hostPort on the Android side, which is bridged to a vsock port on the guest via the podroid-vsock-agent service. The privileged-port limit applies here too, for the same reason.
UDP forwarding works on both backends. Because vsock is a stream transport, each inbound UDP datagram is length-framed and carried over a per-client vsock connection, then delivered to the guest as a real UDP datagram - so a guest DNS server such as Pi-hole is reachable over UDP. Idle flows are reaped automatically, and the number of concurrent client flows is capped to bound resource use.
The privileged-port limit applies at both ends: an unprivileged Android app can't bind a host port below 1024, and rootless Podman can't publish a privileged port inside the guest either. So run a DNS server like Pi-hole on a high port - for example host 5353/udp to guest 5353/udp - and point clients at <phone-ip>:5353.
Podroid is free software (GPL). Docs for v1.2.5. Found something inaccurate? Open an issue.