Network Overlays
Route outbound HTTP calls and spawned processes through SOCKS5, Tailscale, or I2P overlays.
Overview
Wippy supports overlay networks that transparently carry traffic originating from functions, processes, and HTTP clients. Each overlay is a registry entry; code opts in per call, and the selection inherits to inner calls until a descendant explicitly overrides it.
Supported overlays:
network.socks5— generic SOCKS5 proxy (also Tor's SOCKS5 listener)network.tailscale— tsnet overlay nodenetwork.i2p— I2P SAM v3 bridge
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: 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
source: file://probe.lua
method: main
modules:
- io
- http_client
- json
isolate_streams: true makes the SOCKS5 driver mint random credentials per connection so Tor opens a fresh circuit for each dial.
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 on http_client picks the overlay for that call only. Without it the dial goes through the process default (either network_service.default_network in .wippy.yaml or direct).
Step 3: Run It
wippy init
wippy run probe
With Tor running locally:
direct IP: 203.0.113.42
tor IP: 185.220.101.61
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 flows through nested calls. Pick the overlay once at a funcs.call or process.spawn edge and every inner HTTP call, nested funcs.call, and process.spawn underneath uses it until an explicit override:
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
Overlays that support inbound traffic (Tailscale, I2P) can also accept HTTP listeners. Attach the overlay to the http.service instead of the client:
- 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
The server binds on the tailnet interface; clients reach it via the Tailscale address. SOCKS5 is outbound-only — assigning it to http.service is rejected.
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
Explicit selection with network = nil clears the default for that call.
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.
Next Steps
- Network System - Entry kind reference
- HTTP Client - Per-call overlay options
- Security Model - Policies and scopes
- Authentication - Token-based security