Network Overlays
Configure a SOCKS5 overlay for outbound HTTP calls, then review inheritance, inbound listeners, application defaults, and permissions.
Classification: Runnable SOCKS5 tutorial with a partial Tailscale recipe. The direct/Tor probe is complete once an external Tor listener is available. The Tailscale section explains Wippy wiring but intentionally defers account provisioning to Tailscale. For I2P configuration, use the network-system reference linked below.
Overview
Wippy represents overlay networks as registry entries. Code can select an overlay for a call, and that selection propagates to nested calls until a descendant overrides it.
Wippy supports three overlay entry kinds:
network.socks5— generic SOCKS5 proxy (also Tor's SOCKS5 listener)network.tailscale— tsnet overlay nodenetwork.i2p— I2P SAM v3 bridge
Prerequisites
-
Wippy runtime
v0.3.32a. -
curland outbound HTTPS access toapi.ipify.org. -
A Tor daemon exposing SOCKS5 on
127.0.0.1:9050. Install a supported package from the Tor Project download page, start it, and verify the listener before running Wippy:curl --socks5-hostname 127.0.0.1:9050 https://api.ipify.org?format=jsonA successful check returns JSON containing an IP address. Tor Browser commonly uses port 9150 instead; if that is the listener you are intentionally using, change the registry entry and the verification command together.
-
An empty working directory:
mkdir netdemo cd netdemo mkdir src
Project Structure
netdemo/
├── wippy.lock
└── src/
├── _index.yaml
└── probe.lua
Step 1: Define an Overlay
Create src/_index.yaml:
version: "1.0"
namespace: app
entries:
- name: net_policy
kind: security.policy
policy:
actions:
- http_client.request
- network.select
resources: "*"
effect: allow
- name: processes
kind: process.host
lifecycle:
auto_start: true
- name: terminal
kind: terminal.host
lifecycle:
auto_start: true
# SOCKS5 proxy entry (Tor exposes one at 127.0.0.1:9050 by default)
- name: tor
kind: network.socks5
host: 127.0.0.1
port: 9050
isolate_streams: true
- name: probe
kind: process.lua
meta:
command:
name: probe
short: Check outbound IP through overlays
security:
actor:
id: system.probe
policies:
- app:net_policy
source: file://probe.lua
method: main
modules:
- io
- http_client
- json
With isolate_streams: true, the SOCKS5 driver creates random credentials for each connection so Tor can open a fresh circuit for each dial.
Security is strict by default, so the command carries the actor and policy its launch runs under. http_client.request covers the outbound call and network.select covers the explicit overlay choice; without them every check fails closed.
Step 2: Route Outbound Calls
Create src/probe.lua:
local io = require("io")
local http_client = require("http_client")
local json = require("json")
local function fetch_ip(overlay)
local options = { timeout = "15s" }
if overlay then
options.overlay_network = overlay
end
local resp, err = http_client.get("https://api.ipify.org?format=json", options)
if err then
return nil, tostring(err)
end
if resp.status_code ~= 200 then
return nil, "HTTP " .. resp.status_code
end
local body = json.decode(resp.body or "")
return body and body.ip, nil
end
local function main()
local direct, d_err = fetch_ip(nil)
if d_err then
io.print("direct failed: " .. d_err)
else
io.print("direct IP: " .. direct)
end
local routed, r_err = fetch_ip("app:tor")
if r_err then
io.print("tor failed: " .. r_err)
else
io.print("tor IP: " .. routed)
end
return 0
end
return { main = main }
The overlay_network option selects the overlay for that HTTP call. Without it, the dial uses the process default: network_service.default_network from .wippy.yaml, or a direct connection when no default is set.
Step 3: Run It
wippy init
wippy run probe
With Tor running locally:
direct IP: <your public IP>
tor IP: <Tor exit IP>
Both lines must contain valid IP addresses. They should normally differ; the important proof is that the routed request succeeds only through the configured SOCKS listener.
If Tor is not running, the tor IP line will report a dial error — the SOCKS5 overlay does not silently fall back to a direct connection.
Inheritance
Overlay selection propagates through nested calls. Selecting an overlay at a funcs.call or process.spawn boundary applies it to nested HTTP calls, function calls, and process spawns until one explicitly overrides it:
local funcs = require("funcs")
local result, err = funcs.new()
:with_options({ network = "app:tor" })
:call("app:scrape_site", url)
local pid, err = process.with_options({ network = "app:tor" })
:spawn_monitored("app.workers:probe", "app:processes")
The nested function or spawned process sees the overlay on every outgoing dial without passing it explicitly.
Binding a Listener
Tailscale can also accept HTTP listeners. Attach the overlay to the http.service
instead of the client:
- name: bind_policy
kind: security.policy
policy:
actions: "network.bind"
resources: "*"
effect: allow
- name: tailnet
kind: network.tailscale
hostname: wippy-node
auth_key_env: TS_AUTHKEY
ephemeral: true
- name: gateway
kind: http.service
addr: ":8080"
network: app:tailnet
lifecycle:
auto_start: true
security:
actor:
id: system.gateway
policies:
- app:bind_policy
auth_key resolves through the env registry, so TS_AUTHKEY is a registered variable — an OS value needs an env.variable backed by env.storage.os.
Binding through an overlay is gated by network.bind, checked when the listener starts, so the service declares a scope that allows it. The server binds on the tailnet interface; clients reach it via the Tailscale address. SOCKS5 is outbound-only — assigning it to http.service fails the listener with inbound listeners are not exposed over SOCKS5.
App-wide Default
Set a default overlay in .wippy.yaml so every call uses it unless overridden:
network_service:
state_dir: .wippy/net
default_network: app:tor
Permissions
The network.select action gates explicit overlay selection. Deny it on a scope to stop code from choosing an overlay:
- name: deny_network
kind: security.policy
policy:
actions: "network.select"
resources: "*"
effect: deny
groups:
- untrusted
Inherited overlays bypass this check — they were authorized at the caller's edge. Only explicit re-selection at a Lua boundary is gated.
Troubleshooting and Cleanup
connection refusedon127.0.0.1:9050means Tor is not listening on the configured port. Verify Tor with the prerequisitecurlcommand before debugging Wippy.- A direct request failure and a routed success usually indicate local DNS, proxy, or firewall rules affecting the direct path. The two calls are independent.
access deniedfor the routed call means the command security context lacksnetwork.selectforapp:tor; keepapp:probe_policyattached undermeta.command.security.- The SOCKS5 driver never falls back to a direct connection. Do not remove the error merely to make the demo continue.
- Stop the Wippy command when it exits and stop the Tor daemon only if you started it
solely for this tutorial. The SOCKS5 example creates no persistent network state.
A Tailscale entry can persist node state under
.wippy/net/tailscale/; remove the.wippy/netstate directory only after stopping Wippy and only when you intend to discard that local tailnet identity.
Next Steps
- Network System — Entry-kind reference
- HTTP Client — Per-call overlay options
- Security Model — Policies and scopes
- Authentication — Token-based security