Persian / فارسی: راهنمای نسخهٔ Node
The Node.js package (bale-vpn-node/) is a single-binary tool that combines:
- a Bale WebSocket signaling client,
- a local web UI on
http://localhost:3001— auto-opened in your default browser on launch in client mode. All configuration (signing in, picking a peer, activating the tunnel) is done from that page; there is no terminal-based config flow. - a SOCKS5 proxy mode for both client and server,
- a kernel TUN VPN server mode (Linux + macOS) that lets Android clients connect with full IP routing.
The same binary runs on Linux, macOS, and Windows. The VPN-server mode is Linux-only because it relies on a kernel TUN device and iptables MASQUERADE. Set BALE_NO_BROWSER=1 if you want to suppress the auto-open (e.g., headless dev).
| Mode | Platform | What it does |
|---|---|---|
| SOCKS5 client | Linux / macOS / Windows | Listens on 127.0.0.1:1080, tunnels TCP through the chosen Bale contact (the contact must be running a server). |
| SOCKS5 server | Linux / macOS / Windows | Accepts Bale calls from allow-listed contacts and relays the caller's TCP traffic. No kernel TUN, no iptables/pf, no root needed. |
| VPN server (TUN) | Linux / macOS | Accepts Bale calls from allow-listed contacts and terminates raw IP traffic on a kernel TUN device (bale0 on Linux, utunN on macOS), using kernel NAT (iptables MASQUERADE on Linux, pf on macOS) to forward everything to the open internet. |
One binary picks its role at startup: server argument runs as server, no argument runs as client. In server mode on Linux/macOS the binary runs hybrid — accepting both SOCKS5 and TUN-mode clients in parallel; on Windows it serves SOCKS5 only.
Download a prebuilt balevpn-<version>-<platform> binary from the repository's Releases page.
# Linux / macOS:
chmod +x balevpn-*-{linux,macos}-*
./balevpn-... # client mode (default), web UI at http://localhost:3001
./balevpn-... server # server mode
./balevpn-... 3001 # client mode, explicit port (any bare number works)
./balevpn-... server 3001 # server mode, explicit port
# Windows (PowerShell):
.\balevpn-...-win-x64.exe
.\balevpn-...-win-x64.exe server
.\balevpn-...-win-x64.exe 3001Arguments are positional and order-insensitive: any bare number is taken as the web-UI port (default 3001); the literal word server flips role to server. Anything else is ignored.
In client mode the binary auto-opens http://localhost:<port> in your default browser — that's the entire configuration surface (sign in, pick a Bale contact, click Activate). Server mode does not auto-open since servers commonly run on headless hosts.
The web UI binds to
127.0.0.1only — it's never reachable from another machine over the network. For a headless server, use SSH local port forwarding to reach it from your laptop:ssh -L 3001:127.0.0.1:3001 user@your-serverthen open
http://localhost:3001in your local browser. The forwarding tunnel must stay open while you're using the UI.
When you launch in client mode, the binary opens http://localhost:3001 in your default browser. The UI flow:
-
Sign in — phone number → SMS code, or paste an
access_tokenJWT cookie fromweb.bale.aidirectly. The token is persisted server-side in${RUNTIME_DIR}/.bale-vpn_config.json(mode 0600); the browser only ever sees atokenSetboolean, never the JWT itself. -
Tunnel proxy — pick a Bale contact from the list (or search by phone number to pull in a new one), choose a SOCKS5 port (default 1080), and click Activate.
-
The status row turns green when the WebRTC tunnel is up. From this point your apps can use
127.0.0.1:1080as a SOCKS5 proxy and the traffic flows through the chosen contact.
In server mode the UI also shows live Connected clients, a Pending requests queue with Accept/Reject buttons, and an Allowed callers allow-list — see Server admission control below.
SOCKS5 is a per-application TCP relay protocol — your application opens a connection to 127.0.0.1:1080 and the proxy forwards it through the tunnel. This is not a system-wide VPN: only apps you configure to use the proxy go through the tunnel; everything else uses your real internet directly. This is intentional and useful — you can browse one site through the tunnel and everything else locally — but it also means a misconfigured app will silently bypass the tunnel.
Other limitations of SOCKS5 vs. a real VPN:
- TCP only. UDP (including QUIC, some video calls, multicast, etc.) is not relayed. If a site requires QUIC and the browser doesn't fall back to TCP/HTTPS, that site won't work. (The Node tunnel's wire format does carry a UDP-relay frame, but most browsers will not emit UDP through SOCKS5.)
- No ICMP, so
pingfrom your machine still uses your real internet. - No raw IP, so things like WireGuard or other VPN clients running on top will not be tunneled.
If you want full IP routing instead, use the Android app or run the Linux TUN VPN server and connect with the Android client.
The client picks any Bale contact (who must be running a server — Node or Android) and routes TCP via that peer. Steady-state traffic flows on the LiveKit data channel; the Bale WebSocket is only used briefly for signaling and is dropped once the LK channel is up (and brought back up automatically when reconnect signaling is needed).
Once activated, point any SOCKS5-aware app at 127.0.0.1:1080.
⚠️ Use a separate browser (or browser profile) for the tunnel. The local web UI lives athttp://localhost:3001. If you set127.0.0.1:1080as the SOCKS5 proxy in the same browser you used to manage the tunnel, every page in that browser — including the UI itself — will try to go through the proxy. As soon as anything goes wrong with the tunnel, you can no longer reach the UI to fix it. Keep one browser/profile for managing the tunnel and a different one for tunneled traffic.
⚠️ Make sure DNS goes through the proxy too. By default many browsers resolve hostnames locally and only send the IP to the SOCKS5 server — so even though your TCP traffic is tunneled, your DNS lookups still leak to your local resolver, defeating the point of the tunnel for blocked-domain access.The Node SOCKS5 server does support remote name resolution (it accepts SOCKS5 ATYP=0x03 hostnames), so the only thing required is that the client send the hostname instead of resolving it first:
- Firefox — Settings → Network Settings → Manual proxy → SOCKS v5 host
127.0.0.1, port1080, and tick Proxy DNS when using SOCKS v5 (or setnetwork.proxy.socks_remote_dns = trueinabout:config).- Chrome / Chromium / Edge — launch with
--proxy-server="socks5://127.0.0.1:1080". Chrome resolves DNS through the SOCKS5 server when started this way. Browser extensions like FoxyProxy / SwitchyOmega expose the same setting in their UI; make sure "remote DNS" is on.curl— use--socks5-hostname 127.0.0.1:1080(NOT--socks5, which resolves locally first).- Other tools — look for an option named "remote DNS", "SOCKS5h", or "tunnel DNS lookups". The
socks5h://URL scheme is the canonical way to ask for hostname-passthrough.
When you set up another Node instance as the client side (i.e., running this app and connecting to a remote Node/Android server), the built-in SOCKS5 listener follows the same rule: it accepts ATYP=0x03 (hostname), ATYP=0x01 (IPv4), and ATYP=0x04 (IPv6). The hostname is forwarded verbatim to the server, which performs the DNS lookup. The app never resolves DNS locally before tunneling, so there is no DNS leak from the Node client itself. DNS leakage on your machine therefore comes only from the application configuration (your browser settings above).
Application-layer encryption (HTTPS / TLS) is what actually keeps your payload private from the server peer and from Bale's LiveKit infrastructure. See the privacy note in the main README.
Run the binary with the server argument. It listens for incoming Bale calls, auto-answers (subject to the allow-list — see below), and acts as a TCP relay for the caller. No system privileges needed.
./balevpn-... serverThis is the easiest way to give a friend a relay if you don't want to set up a VPN. The caller uses Node SOCKS5 client mode (or any SOCKS5 client) and gets transparent TCP forwarding.
This is the highest-throughput server option, available on Linux and macOS. The Node process attaches to a kernel TUN device (bale0 on Linux, utunN on macOS) and the kernel handles forwarding + NAT, which is substantially faster than the userspace TCP/IP stack used by the Android server.
Recommended pairing: Node TUN server + Android client for the best connection on the client side (Android VpnService ships every IP packet from the device into the tunnel).
# 1. Allow the released binary to manage TUN interfaces without running as root.
# Apply setcap to the actual file you'll execute — NOT to /usr/bin/node.
sudo setcap cap_net_admin+eip ./balevpn-<version>-linux-x64
# 2. Tell the kernel to NAT traffic from the tunnel subnet out the host's
# real interface (run once; survives reboots only if added to your
# distro's iptables-save / firewalld config)
sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -j MASQUERADEThen run:
./balevpn-<version>-linux-x64 servermacOS has no setcap analog, so server mode runs as root. NAT (pf anchor balevpn) and IP forwarding are loaded automatically on startup; the WAN interface is auto-detected via route -n get default.
sudo ./balevpn-<version>-macos-arm64 server- Removes any stale TUN interface from a prior run.
- Creates the TUN device and assigns
10.8.0.1/24. - Enables IPv4 forwarding (
/proc/sys/net/ipv4/ip_forwardon Linux,net.inet.ip.forwardingon macOS). - Loads the platform-specific NAT rule (iptables MASQUERADE on Linux, pf anchor on macOS).
- Starts receiving incoming Bale calls — allow-listed callers connect automatically; everyone else lands in a pending queue (see Server admission control).
When an Android client connects, it gets 10.8.0.2/24 and routes all of its traffic into the tunnel. Up to 253 clients can connect simultaneously — the server transparently rewrites each client's source address to a distinct IP in 10.8.0.0/24 so the kernel can disambiguate concurrent flows.
- IPv4 only. IPv6 packets from the client are explicitly dropped (the Android client falls back to IPv4 fast via ICMPv6 Destination Unreachable).
Whether running as SOCKS5 server or TUN VPN server, every incoming call from a contact who isn't on the allow-list lands in a pending queue. The web UI surfaces it as a yellow row with Accept once / Allow always / Reject buttons.
- Accept once handles this single call but doesn't persist the caller. Future calls land in pending again.
- Allow always adds the caller to the allow-list (persisted server-side in
${RUNTIME_DIR}/.bale-vpn_config.jsonunder theadmissionkey, mode 0600). Future calls from the same caller auto-accept. - Reject sends a
DiscardCallso the caller's tunnel tears down immediately and adds the caller's id to the block-list — future calls from this id are silently rejected (no notification, no pending entry). Undo via the Blocked callers list shown below the pending queue (per-row Unblock button). - Pending entries auto-reject after 60 s. The 60-second timeout does NOT blacklist — only an explicit Reject does.
Two ways to get an access_token JWT into the app:
- Phone OTP via the UI — enter your phone, type the SMS code, the binary fetches the cookie via the standard
web.bale.ai/set-cookie/?jwt=…flow and persists it in${RUNTIME_DIR}/.bale-vpn_config.json(mode 0600). This is the recommended path. - Paste a token — copy the
access_tokencookie from a logged-inweb.bale.aiChrome session (DevTools → Application → Cookies) and paste it into the textarea on the UI.
WebSocket close code 4401 means the token expired; sign in again.
The LiveKit data channel is encrypted with DTLS, so traffic is opaque to passive observers on the network. However, Bale's LiveKit servers act as the SFU/TURN node and have access to the plaintext media — they can see your destinations and any unencrypted application payload. Use TLS at the application layer (HTTPS, encrypted DNS, etc.) and treat this tunnel like a VPN whose operator you don't fully trust.
See the main README for a fuller note.




