Platform support and TUN mode
TUN mode
client_mode: "tun" (CLI --mode tun) is the supported way to tunnel a whole device. The client creates a virtual interface and runs a userspace TCP/IP stack. Every TCP connection and UDP flow the operating system sends into the interface is terminated locally and carried as a MEHS flow to the server, which opens the real connection. The server therefore needs no raw-IP gateway and no privileges. DNS (UDP/53) is just UDP; ICMP echo is answered locally.
- Default addresses are
198.18.0.1/16andfd77:6d65:6873::1/64on the interface; full-tunnel routes are0.0.0.0/1,128.0.0.0/1,::/1, and8000::/1, which take precedence over the default route without removing it. The control and data server addresses are pinned to the physical path first, so the tunnel cannot route into itself. - Routes and bypass routes are removed on orderly exit (Ctrl-C or FFI stop). If the process is killed, cleanup on Linux is verified; the next client start also removes stale routes.
- The interface and routes stay up across reconnects, so traffic does not leak onto the direct path while the data session is re-established. In-flight connections are reset at that moment.
- UDP/443 (QUIC/HTTP3) from applications is dropped by default (
block_quic) so browsers use TCP, which performs far better over a reliable carrier. UDP otherwise travels over the reliable, ordered carrier, so it inherits head-of-line blocking; real-time calls work but are best on the QUIC carrier. - Limits: up to 2048 concurrent TUN connections per client (the server's
max_flows_per_sessionalso applies) and 1024 UDP NAT entries with a 60-second idle timeout.
| Platform | Mechanism | Needs | Status |
|---|---|---|---|
| Linux | /dev/net/tun, routes via ip | root or CAP_NET_ADMIN, iproute2 | Verified end to end (TCP and QUIC carriers: HTTPS, HTTP, DNS, ICMP, 10 MB download, route cleanup) |
| Windows | Wintun, routes via PowerShell New-NetRoute | Administrator, wintun.dll of the same architecture next to the executable | Manually verified by maintainers; runtime coverage is limited. |
| macOS | utun, routes via route | root | Implemented; not runtime-tested. |
| Android | Host-integrated VpnService descriptor passed with --tun-fd | Host owns consent, foreground service, addresses, routes, DNS, UID exclusion, and revoke handling | Implemented; host integration not runtime-tested on device. |
| iOS | NEPacketTunnelProvider packet-flow callbacks | Host configures tunnel addresses, routes, and DNS | Implemented; host integration not runtime-tested on device. |
See Flutter integration for embedding and the CLI guide for TUN invocation examples. Do not run TUN mode while another VPN or proxy tool that manages a TUN device and default route (for example Happ/xray) is active.
Legacy packet bridge
The legacy packet bridge predates MEHS TUN mode. It is intended only for hosts that already own a packet device and operate a compatible remote raw-IP UDP gateway; MEHS does not provide that gateway. New integrations should use TUN mode.
The bridge contract is to expose an MTU, asynchronously read and write complete IP packets, and release device resources on shutdown. It forwards each complete IPv4 or IPv6 packet as one MEHS datagram payload on an already-opened datagram flow, and injects valid response packets received on that same flow. It rejects invalid MTUs, malformed or oversized packets, applies flow/device backpressure, and shuts the adapter down when the run exits.
The remote flow target must be a UDP endpoint implementing the raw-IP gateway contract: accept an IP packet as a datagram and return an IP packet as a datagram. The normal MEHS SOCKS/UDP proxy endpoint forwards application UDP payloads and is not such a gateway. A compatible remote gateway is required for end-to-end traffic through this legacy bridge.
Host responsibilities
The embedding host, not the packet adapter or bridge, configures interface addresses, MTU, DNS, routes, and policy routing. The host must:
- Create/configure the platform tunnel interface and select split-tunnel or full-tunnel routes.
- Keep the MEHS carrier's route on the physical network. For split tunneling, do not include the carrier endpoint in tunneled prefixes. For full tunneling, install a host route or policy-routing exclusion for every carrier address before diverting the default route. Otherwise the carrier can route into its own tunnel and stall.
- For the legacy packet bridge, open a MEHS datagram flow to the remote IP gateway, then run the bridge with the configured adapter and flow.
- On teardown, stop/drop the bridge and restore host routes and DNS. Follow the lifecycle sequence below for sleep, wake, and network changes.
The bridge is bounded by its packet buffer and the session flow's bounded queues. It reads one outbound packet at a time and continues servicing inbound packets while a send is waiting for flow capacity. Oversized or malformed packets are discarded; adapter I/O and flow failures end the bridge and trigger adapter shutdown. Aborting/dropping the bridge still drops the platform adapter, whose host implementation must release native handles; hosts needing an orderly asynchronous close should let it finish or call adapter shutdown before dropping it.
Sleep, wake, and network-change lifecycle
The host owns OS event observation and must serialize lifecycle notifications for each client handle. OS notifications can be repeated, coalesced, or missed while the app is suspended, so treat them as prompts to re-check the current system state rather than as a complete event log.
- Before sleep: call
mehs_on_sleep(orPlatformLifecycle::on_sleep) and pause probes, packet forwarding, and creation of new links. Keep the logical MEHS session state; do not tear it down just because the device is sleeping. - After wake: call
mehs_on_wake(orPlatformLifecycle::on_wake), then check the authenticated carrier path and the host's carrier-exclusion routes. Keep probes available for recovery, but keep new traffic paused until the path is healthy. Callmehs_on_wake_path_health(client, 1)orcomplete_wake_health_check(true)only after that check succeeds. If it fails, keep traffic paused, update network identity if it changed, and recover or reselect the carrier before checking again. A failed health check does not complete the pending wake. - On interface/path change: obtain the current interface index and a deterministic, non-secret fingerprint of the relevant network identity (for example, active interface and assigned addresses). Call
mehs_on_network_changeorPlatformLifecycle::on_network_change. When the identity changes, invalidate cached path-selection results, reselect or reconnect the carrier, and update host routing. The FFI API exposes the change throughmehs_client_network_generation; it does not invalidate an embedding's separate cache for it. - After recovery: resume forwarding and normal probe cadence only after the carrier is healthy and routes still keep the carrier outside the tunnel. Recreate the packet adapter/bridge only when the OS replaced the underlying device or its path cannot be repaired in place.
The mehs_on_* FFI lifecycle functions operate on a MehsClient handle. The separate mehs_runtime_* API used to run the client supervisor exposes start, status, and stop, but does not expose these lifecycle notifications. The standalone client has no sleep/wake command or OS-event subscription; it reconnects in response to transport failures. Hosts that need proactive pause and health-check behavior must integrate the lifecycle callbacks with an API that exposes them.
OS event sources
These are event sources for the embedding, not OS behavior implemented by MEHS. Re-query active interfaces, addresses, and routes after wake or a path event; the notification itself does not establish that the MEHS carrier is healthy.
| OS | Sleep/wake signals | Network-change signals |
|---|---|---|
| macOS | Observe NSWorkspace.willSleepNotification and didWakeNotification on NSWorkspace.shared.notificationCenter. | Use NWPathMonitor for path updates, then re-read the selected interface and addresses. See NSWorkspace and NWPathMonitor. |
| Windows | Handle WM_POWERBROADCAST suspend/resume events, such as PBT_APMSUSPEND and PBT_APMRESUMEAUTOMATIC; resume may be reported before user activity. | Use IP Helper notifications such as NotifyIpInterfaceChange and re-query interface/address state. See power broadcasts and NotifyIpInterfaceChange. |
| Linux | On systemd systems, subscribe to logind's PrepareForSleep signal (true before sleep, false on resume). There is no single desktop-independent sleep notification API. | Subscribe to rtnetlink link/address/route updates (RTM_NEWLINK, RTM_NEWADDR, and route notifications), or use the active network manager's API. See PrepareForSleep and rtnetlink(7). |
Mobile hosts should also re-check state when their service or tunnel extension resumes. Android hosts can observe ConnectivityManager.NetworkCallback; iOS Network Extension hosts should use the provider's lifecycle and path-monitoring facilities. Neither platform guarantees that an app receives a desktop-style sleep callback while suspended. See Android network callbacks and iOS integration.
Platform adapters
- Linux: the Linux adapter opens
/dev/net/tunwithIFF_TUN | IFF_NO_PI. It does not configure the interface or networking. Example split-tunnel setup:ip link set dev mehs0 mtu 1400 ip addr add 10.77.0.1/24 dev mehs0 ip link set dev mehs0 up ip route add 198.51.100.0/24 dev mehs0Before using a full-tunnel default route, add a route/policy exclusion that keeps the MEHS carrier endpoint on the physical interface.
- Android: the Android adapter takes ownership of a TUN file descriptor from
VpnService.Builder.establish()(for example, viaParcelFileDescriptor.detachFd()). The Android host configures addresses, routes, DNS, and the carrier exclusion throughVpnService.Builder; the adapter must be given the host-provided descriptor. - Windows: the Windows adapter uses an existing Wintun adapter. The host installs/configures Wintun, sets the interface MTU (1400), addresses, DNS, routes, and a carrier exclusion. Provide the absolute path to the official, unmodified Wintun DLL; loading a DLL executes code in-process.
- macOS: the macOS adapter opens a dynamically numbered
utuninterface from a Tokio runtime. The host configures its address, MTU (1400), DNS, routes, and a carrier exclusion. Packet I/O handlesutun's four-byte address-family header. - iOS: the legacy iOS TUN adapter is unsupported. The supported integration is the callback-backed TUN path: the host passes packets read from
NEPacketTunnelFlowto MEHS and supplies a callback for packets written back to the flow. See iOS integration. The host ownsNEPacketTunnelNetworkSettings, DNS, routes, App Group setup, and carrier bypass routes.
Generic bridge behavior has host-level unit coverage; each release's checks cover only adapters available for its host target. This does not claim tested end-to-end VPN lifecycle behavior on each OS. Platform release support requires native builds and host-app tests for sleep/wake, network changes, adapter failure, and teardown.