Skip to content

Releases: theta42/jump-host

v3.1.6

Choose a tag to compare

@wmantly wmantly released this 12 Aug 17:04
dc98abb
  • chore: picks up @simpleworkjs/frontend 0.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

Choose a tag to compare

@wmantly wmantly released this 12 Aug 07:26
1da2eee
  • feat: the shared UI shell loads app.sync.js and app.filter.js from @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 null body, and the client tagged it unconditionally — throwing and killing the socket handler.

v3.1.4

Choose a tag to compare

@wmantly wmantly released this 12 Aug 05:37
b944da5

v3.1.4

  • The loopback Redis config is actually loaded now. install.sh wrote /etc/redis/theta-gateway.conf (loopback bind, dir under /var/lib/theta-gateway/redis, AOF on) but merely systemctl 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 points redis-server at 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/iptables and 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. applyExits now 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.sh bare-metal path and docker compose logs -f jump-host were all dead. Now documents install.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

Choose a tag to compare

@wmantly wmantly released this 12 Aug 03:37
da73926

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/32 prints as from 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 read planned.exits and planned.exitRules — the
    planner's output — which is why they passed happily while applyExits was
    never being reached at all. They now read the live WireGuard device and the
    kernel's own routing rules, via a new exitRulesLive on
    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

Choose a tag to compare

@wmantly wmantly released this 12 Aug 03:16

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=1 to manage the runtime yourself.
    Verified from nothing on a bare debian:bookworm-slim.

v3.1.0 — the gateway installs on the host

Choose a tag to compare

@wmantly wmantly released this 12 Aug 02:54
57baa95

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 the theta-gateway service. Idempotent, so
    re-running upgrades in place; --uninstall deliberately 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,
      setting net.* sysctls in the init namespace and installing NAT/NETMAP
      rules all need CAP_NET_ADMIN there, and sysctl writes are effectively
      root-only. A capability-scoped user buys ambiguity, not safety.
  • fix: the router threw instead of degrading, and it cost the exits. The
    container image shipped without iptables or procps, and /proc/sys is
    read-only in a default container — so every NAT, forwarding and NETMAP call
    failed. applyForwarding was the one unguarded call in applyPlan, so it
    threw mid-reconcile: the NETMAP loop never ran, applyPlan never returned,
    and applyExits was never reached. Exits were planned and silently never
    applied. net_router now detects missing tooling once, says what that costs,
    and configures everything it still can; sysctls fall back to writing
    /proc/sys directly 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

Choose a tag to compare

@wmantly wmantly released this 12 Aug 02:16
9da5b2e

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>/32 for a gateway's identity,
    10.<siteId>.0.0/16 for everything at that site, 10.<s>.128.0/17 for
    devices, 10.<s>.168.0/24 and 10.<s>.172.0/24 for 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,
    and rp_filter=0 — without which policy-routed exit traffic silently
    blackholes while every other diagnostic looks healthy. Every iptables rule is
    added behind a -C check, since reconcile runs at boot, on every roster
    change, and on a timer.
  • feat: per-device internet exits, one WireGuard interface each.
    AllowedIPs is a single trie per interface, so only one peer can own
    0.0.0.0/0 and 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 with wg 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-key leaves 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/16 pool 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

Choose a tag to compare

@wmantly wmantly released this 11 Aug 15:56
4219e6c
  • 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 the 172.24.<idx>.1 mesh IP a gateway reports is unreachable from the sibling containers (theta-directory, theta-proxy) that were told to use it, and nothing listened on :3001 there 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: ingress 172.24.<own>.1:3001 -> sso-manager:3001 (override with THETA_MESH_SERVICE_TARGET), egress 0.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 in utils/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

Choose a tag to compare

@wmantly wmantly released this 11 Aug 01:22

v2.1.1

  • fix: mesh peer removal now cleans up its kernel routes. wg_iface.removePeer() dropped the WireGuard peer entry but left the ip route entries setPeer() had added, so a removed peer's subnet stayed routed into a dead tunnel. Fixed by querying wg show <iface> allowed-ips before removal and ip route del-ing each CIDR. Verified live: routes present after setPeer, gone after removePeer, this gateway's own local route untouched. Exposed via DELETE /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/register was unreachable via HTTP. routes/api.js mounted / (admin-session-gated routes/jump.js) before /mesh; since router.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 before routes/mesh.js ever ran. A real gateway-to-gateway /join call failed with a checkApiToken/LoginFailed error instead of registering. Found live-testing the new /self endpoint with two real containers; fixed by mounting /mesh first.
  • fix: the initiating side of a mesh join never recorded its own identity. POST /register (the receiving side) persists a (self) registry entry via ensureOwnMeshIndex(), but POST /join (the initiating side) never did, so GET /api/mesh/self and 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's register() now accepts an explicit meshIndex instead 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

Choose a tag to compare

@wmantly wmantly released this 10 Aug 23:45

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) — prefers ip link add type wireguard, falls back to wireguard-go when the kernel module isn't available.
  • mDNS local-discovery announcer (services/mdns_announce.js) — advertises which public hostnames this site fronts so a theta-agent on 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