Releases: theta42/jump-host
Releases · theta42/jump-host
Release list
v3.1.6
- chore: picks up
@simpleworkjs/frontend0.3.1 (filtered rows carrying a
Bootstrap display utility are actually hidden; the filter count no longer goes
stale). No view here uses the filter layer yet, so this is a dependency
alignment rather than a behaviour change.
v3.1.5
- feat: the shared UI shell loads
app.sync.jsandapp.filter.jsfrom@simpleworkjs/frontend, so views here can adopt the same live-update and filtering layers as the other apps. - chore: removed the client-side publish forwarding from
app-base.js. It echoed every local publish back over the socket for the server to fan out. This app attaches socket.io only to serve the client library, so unlike proxy and theta-directory there was no bridge to secure — but the code was dead weight, and its twin in the other two apps was a topic-injection path. - fix: a delete event carries a
nullbody, and the client tagged it unconditionally — throwing and killing the socket handler.
v3.1.4
v3.1.4
- The loopback Redis config is actually loaded now.
install.shwrote/etc/redis/theta-gateway.conf(loopback bind,dirunder/var/lib/theta-gateway/redis, AOF on) but merelysystemctl enable --now redis-server, which loads the distro default — so the gateway's WireGuard identity, sessions and OAuth state persisted to a path nobody documented or backed up, and AOF durability was never enabled. A systemd drop-in now pointsredis-serverat the gateway config. - Reconcile is serialized. A pass dials the directory three times (10s timeout each) and can outlive the 60s interval; overlapping passes raced on
wg/ip/iptablesand could stack duplicate rules. A tick that finds a pass in flight now skips instead of stacking on it. - Exit policy rules are diffed, not cleared-and-re-added. The old clear-all-re-add approach blipped every exiting device's routing for a window on each 60s pass, and its cleanup deleted any rule in the priority range regardless of owner.
applyExitsnow removes only stale rules by exact match and adds only missing ones — no blip, and an operator rule that lands in the range is left alone. - Docs rewritten for the host-installed gateway (
DEPLOYMENT.md,docs/installation.md): the container-era "three ways to run" instructions,ops/install.shbare-metal path anddocker compose logs -f jump-hostwere all dead. Now documentsinstall.sh, where config/data live, the Redis drop-in, port-22 options and the roster-driven mesh.
v3.1.2 — the e2e checks exits are applied, not planned
v3.1.2
- fix: exit routing rules now read back in the form they were added. The
kernel drops the prefix on a host rule — one added as
from 10.2.128.1/32prints asfrom 10.2.128.1— so anything comparing the
installed rules against the intended ones saw a mismatch that was not there. - test: the end-to-end suite checks that exits are APPLIED, not planned.
Its exit assertions readplanned.exitsandplanned.exitRules— the
planner's output — which is why they passed happily whileapplyExitswas
never being reached at all. They now read the live WireGuard device and the
kernel's own routing rules, via a newexitRulesLiveon
GET /api/mesh/status, and assert the exit tunnel completed its own
handshake under the separate exit key.
v3.1.1 — install Node instead of refusing to start
v3.1.1
- fix: install Node instead of refusing to start without it. The gateway
used to run in a container carrying its own runtime, so a host never needed
one; running on the host means it does, and v3.1.0 stopped with "node is
required" partway through a setup that had already brought the rest of the
stack up. It now installs Node 22 from NodeSource on apt hosts (Debian and
Ubuntu ship 18, below the engines floor, so the distro package is not an
option).THETA_SKIP_NODE_INSTALL=1to manage the runtime yourself.
Verified from nothing on a baredebian:bookworm-slim.
v3.1.0 — the gateway installs on the host
v3.1.0
The gateway installs on the host now, not in a container.
- feat:
install.sh+ a systemd unit. Installs dependencies, lays the app
down in/opt/theta-gateway, writes/etc/theta-gateway/gateway.env, binds
Redis to loopback, and enables thetheta-gatewayservice. Idempotent, so
re-running upgrades in place;--uninstalldeliberately keeps
/var/lib/theta-gateway, since that holds this gateway's WireGuard identity
and every peer in the cluster has its public half.- Refuses to start if the SSH port collides with the host's own
sshd,
rather than "succeeding" and leaving the operator locked out. - Runs as root: creating WireGuard interfaces, writing the routing table,
settingnet.*sysctls in the init namespace and installing NAT/NETMAP
rules all needCAP_NET_ADMINthere, and sysctl writes are effectively
root-only. A capability-scoped user buys ambiguity, not safety.
- Refuses to start if the SSH port collides with the host's own
- fix: the router threw instead of degrading, and it cost the exits. The
container image shipped withoutiptablesorprocps, and/proc/sysis
read-only in a default container — so every NAT, forwarding and NETMAP call
failed.applyForwardingwas the one unguarded call inapplyPlan, so it
threw mid-reconcile: the NETMAP loop never ran,applyPlannever returned,
andapplyExitswas never reached. Exits were planned and silently never
applied.net_routernow detects missing tooling once, says what that costs,
and configures everything it still can; sysctls fall back to writing
/proc/sysdirectly when the binary is absent. - The mesh, device tunnels and site-to-site routing always worked in a
container. What did not was everything touching the host network — which is
most of what a router does.
v3.0.0 — the gateway becomes a router
v3.0.0
Breaking. The gateway no longer holds any network configuration of its own.
Sites, devices, LAN mapping and exits are configured in theta-directory; the
gateway publishes its public key and endpoint and applies whatever the roster
says. Addressing changed with it, so every site must be rebuilt.
- feat!: the gateway is a roster-driven router. Joining the directory is
joining the mesh — the mint-token/register/join dance, the per-gateway
registry and its independently-allocated index are gone, along with the
second allocator that could disagree with the first. A site's id is its
ldapServerId, allocated once, cluster-wide, when it joined. - feat!: new addressing.
172.24.0.<siteId>/32for a gateway's identity,
10.<siteId>.0.0/16for everything at that site,10.<s>.128.0/17for
devices,10.<s>.168.0/24and10.<s>.172.0/24for LAN mapping. One octet
per site caps a cluster at 254 — below LDAP's own 4094 ServerID ceiling, so
the addressing is now the binding constraint. - feat: full router.
ip_forward, MASQUERADE on the auto-detected uplink,
stateful FORWARD rules, NETMAP of each site's physical LAN into a shadow /24,
andrp_filter=0— without which policy-routed exit traffic silently
blackholes while every other diagnostic looks healthy. Every iptables rule is
added behind a-Ccheck, since reconcile runs at boot, on every roster
change, and on a timer. - feat: per-device internet exits, one WireGuard interface each.
AllowedIPsis a single trie per interface, so only one peer can own
0.0.0.0/0and the last to claim it silently takes it from the others; and
WireGuard routes on destination, ignoring the kernel nexthop, so
default via <peer>cannot select among exits. Devices are steered with
ip rule from <device>/32, so changing an exit rewrites one rule and never
touches the device — no reconnect, no reissued config. - fix: registering a second peer used to delete the first.
setPrivateKey
applied the key withwg setconf, which replaces the whole device config and
removes every peer not in it. Verified against wireguard-go in the gateway
image: setconf takes 1 peer to 0,wg set private-keyleaves 2 at 2. - fix: the mesh never came back after a restart. The registry was durable
and the interface was not, and nothing rebuilt one from the other. Now
reconciled at boot and on a timer. - fix:
'(self)'was trusted for identity. That slug arrived in a remote
gateway's own registration body, so a peer could claim to be us. Identity now
comes from the public key, which never crosses the wire. - feat: directory config is cached in Redis and reconcile falls back to it, so
the gateway keeps routing through a directory outage. - feat: the mesh UI is now diagnostics — what the roster asked for, what is on
the wire, and where they disagree, with named callouts for the states that
otherwise look identical to healthy (no site id, stale config, interface
down, no uplink, no NETMAP support, an unbuildable exit). - removed:
services/mesh_forwarder.js. It bridged traffic in userspace on a
port derived from the site index because WireGuard was trapped inside a
container namespace. With real routing a peer site's directory is just
10.<n>.0.2:3001. - removed: the roaming-client feature (
routes/wireguard.js,wg_peer,
wg_site,wg_conf). It handed out configs and QR codes that could never
connect — no interface ever received a peer entry for them, and the endpoint
advertised pointed at the mesh interface, which held the same key but no
matching peer, so every config downloaded failed its handshake silently.
Devices are directory-managed now, with keys the server never stores. Its
10.100.0.0/16pool went too; that range collided exactly with a site
landing on id 100. - fix: exit interfaces get their own keypair. A remote gateway keeps one
endpoint and one session per peer KEY, so an exit interface presenting the
same key as the mesh interface made the remote's single peer entry flap
between the two while they invalidated each other's session — intermittent
breakage rather than a clean failure. Verified against wireguard-go in the
gateway image: one key on two interfaces left the remote pointed at whichever
handshook last, with both still re-handshaking; separate keys give two stable
peers. The gateway now publishes a second exit key, and an exit site builds a
peer entry for anyone exiting through it — allowed only the specific device
addresses using that exit, since an exit is permission to send internet
traffic, not a route into a network. - test: three-gateway end-to-end over real WireGuard
(docker-compose.mesh-e2e.yml). Three and not two deliberately — the
peer-wipe and index bugs above are both structurally invisible with one peer
per gateway.
v2.2.0 — mesh service forwarding
- feat: mesh service forwarding (
services/mesh_forwarder.js) — the data plane the mesh control plane assumed but never had. WireGuard runs inside this container's network namespace, so the172.24.<idx>.1mesh IP a gateway reports is unreachable from the sibling containers (theta-directory, theta-proxy) that were told to use it, and nothing listened on:3001there in any case. Relay routes and mesh-preferred replication both pointed at a dead target while the existing ICMP-level tests passed. Now bridged in userspace both ways: ingress172.24.<own>.1:3001 -> sso-manager:3001(override withTHETA_MESH_SERVICE_TARGET), egress0.0.0.0:<30000+peer> -> 172.24.<peer>.1:3001. Ports are derived from the peer's mesh index, so nothing is stored or discovered — theta-directory computes the same number inutils/mesh_route.js. Forwarders reconcile on every mesh change, so a new peer is reachable without a restart and a removed one stops being reachable immediately. - test:
docker-compose.mesh-e2e.yml+test/mesh_data_plane_e2e.js— two real gateways over real WireGuard, asserting an HTTP request crosses the tunnel and reaches the far site's service (both directions), and that a removed peer's forwarder stops serving.
v2.1.1
v2.1.1
- fix: mesh peer removal now cleans up its kernel routes.
wg_iface.removePeer()dropped the WireGuard peer entry but left theip routeentriessetPeer()had added, so a removed peer's subnet stayed routed into a dead tunnel. Fixed by queryingwg show <iface> allowed-ipsbefore removal andip route del-ing each CIDR. Verified live: routes present aftersetPeer, gone afterremovePeer, this gateway's own local route untouched. Exposed viaDELETE /api/mesh/gateways/:id+ a remove button in the mesh UI. - feat:
GET /api/mesh/self— this gateway's own mesh IP, gated by any valid self-service API token rather than a full jump-admin session, so an unattended local script (e.g.theta-suite's no-inbound relay bootstrap) can discover it without admin credentials. - fix:
/api/mesh/registerwas unreachable via HTTP.routes/api.jsmounted/(admin-session-gatedroutes/jump.js) before/mesh; sincerouter.use('/', ...)matches every/api/*path, every/api/mesh/*request — including/register, authenticated by a bearer mesh join token, not an admin session — hit that admin gate first and 401'd beforeroutes/mesh.jsever ran. A real gateway-to-gateway/joincall failed with acheckApiToken/LoginFailederror instead of registering. Found live-testing the new/selfendpoint with two real containers; fixed by mounting/meshfirst. - fix: the initiating side of a mesh join never recorded its own identity.
POST /register(the receiving side) persists a(self)registry entry viaensureOwnMeshIndex(), butPOST /join(the initiating side) never did, soGET /api/mesh/selfand the mesh UI's own-entry handling silently saw nothing on whichever gateway called/join. Fixed by registering a self-entry there too, using the exact mesh index the remote assigned (models/mesh_gateway.js'sregister()now accepts an explicitmeshIndexinstead of always auto-picking one from the local registry). Verified with two real meshed containers: both sides now report their own correct mesh IP.
Theta Gateway v2.1.0
Added
- Gateway-to-gateway WireGuard mesh (
routes/mesh.js) — real site-to-site tunnels between theta-gateway instances, distinct from the existing roaming-client/exit-node WireGuard feature. Join-token bootstrap, mesh-index addressing (172.24..0/16 + 10..0.0/16). - In-kernel WireGuard with a userspace fallback (
utils/wg_iface.js) — prefersip link add type wireguard, falls back towireguard-gowhen the kernel module isn't available. - mDNS local-discovery announcer (
services/mdns_announce.js) — advertises which public hostnames this site fronts so atheta-agenton the same LAN segment can skip the relay/WAN path. - Mesh UI (
/mesh) — gateway identity, join-token minting, remote-join form, meshed-gateways table.
Verified with real two-container tests: an actual encrypted WireGuard tunnel passing ICMP traffic end to end (0% loss), and the mDNS announce/discover/apply/revert cycle over real multicast. Two real bugs found and fixed along the way — see CHANGELOG.md.
Full changelog: https://github.com/theta42/jump-host/blob/master/CHANGELOG.md