Skip to content

Concepts

Per-session VPN

How each device gets its own switchable egress location.

Every emulator session can route all of the device’s traffic (TCP, UDP and DNS) through a VPN, and the user can switch the location from the session toolbar at any time.

The session Pod has a vpn sidecar running sing-box in the Pod’s network namespace, which Android shares:

  • sing-box opens a tun interface, and the agent adds policy routing inside Android that sends every app’s traffic into it (Android’s own network daemon manages the routing there, so sing-box can’t);
  • cluster and LAN ranges (vpn.bypassCIDRs) are excluded, and so is everything running as root (the agent, sing-box itself, Android’s root daemons), so the portal can still reach the agent;
  • each location is a sing-box outbound, plus direct, behind a selector;
  • switching location changes the selector through sing-box’s local API, so the switch is instant and needs no restart;
  • the device’s DNS queries are captured from the tunnel and handed to the session’s agent, which logs them, answers your rewrites and resolves the rest through the selected location, so lookups don’t leak (see DNS log and rewrites);
  • those lookups return IPv4 addresses only (AAAA answers are empty), so the device never tries IPv6 through a location that can’t carry it.

The launch dialog shows a location’s flag when its id or label is a country code or name. An app listed in a location’s defaultFor starts in that location. Otherwise the dialog and the quick-launch button use the location you used last time with that provider, or with Any (remembered in the browser’s localStorage, also when you switch during a session). If there is none, or it is no longer offered, it falls back to the configured default (vpn.default). With vpn.direct: false there is no VPN-less option at all.

The same tunnel carries the session’s network conditions: latency, bandwidth limits, packet loss and outages applied with tc netem, and regions with their own round trips to CDN edges and to servers.

Android itself is barely configured (only Private DNS is turned off, so lookups stay visible to the agent); to apps it looks like a normal network. Port forwarding (adb reverse, your connected computer) runs over loopback and is unaffected by the VPN.

Holtdroid has no code for any particular VPN vendor. A location is an id, a label and a sing-box outbound (or endpoint, such as WireGuard): SOCKS, Shadowsocks, VLESS, Hysteria, WireGuard, anything sing-box speaks. Holtdroid only tags it with the location id and puts it behind the selector.

Every session Pod runs its own copy of these outbounds. That is fine for proxies that accept many connections with the same credentials, but not for WireGuard: VPN services usually give one key per account, and the same key connected from several Pods at once makes the peers knock each other off.

Run one sing-box yourself as a VPN gateway. It holds the real VPN connection, one tunnel per location, and offers a SOCKS server where the username selects the location. Each Holtdroid location is then just “SOCKS to the gateway, user de”:

flowchart LR
  subgraph s1["Session Pod 1"]
    e1["emulator"] --> v1["sing-box sidecar"]
  end
  subgraph s2["Session Pod 2"]
    e2["emulator"] --> v2["sing-box sidecar"]
  end
  v1 -- "socks, user de" --> gw["VPN gateway<br/>(your sing-box)"]
  v2 -- "socks, user nl" --> gw
  gw -- "WireGuard" --> de[(VPN server DE)]
  gw -- "WireGuard" --> nl[(VPN server NL)]
  • One key per location serves any number of sessions.
  • Switching stays per session: each sidecar has its own selector.
  • Sessions on the same location share its egress IP.
  • Adding a vendor or a country is a change to the gateway, not to Holtdroid.

Shared VPN gateway has a complete setup. Configuration keys are on the emulator provider page.