janus

package module
v1.19.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 29, 2026 License: Apache-2.0 Imports: 74 Imported by: 0

README

Janus Logo

Caddy module: long-lived edge server — TLS admission, dynamic host routing, registry-driven upstreams, heartbeats, on-demand TLS asks, edge-terminated WebSocket fan-out, zero-config LAN presence over mDNS, an edge authentication wall, registered static files and directory browsing, X-Sendfile offload, and bounded app-scoped access observation, driven by a JSON control API.


Module names: janus (app) · http.handlers.janus (HTTP handler) · tls.permission.janus (on-demand TLS permission) · caddy.logging.encoders.janus (access encoder)

Janus is a Caddy module. Caddy provides listeners, HTTP/1–3, TLS, and ACME. Janus provides the inward face: a memory-resident registry and engines driven by the /1.0 JSON API. Cold Caddyfile config sets capabilities (such as control reachability) and which sites admit traffic into Janus; hot /1.0 calls decide how admitted hosts map to upstreams, health, certificate allowlisting, and realtime fan-out.

{
	janus {
		ping
		control local
		hub
		mdns
		files {
			precompressed
		}
	}
}

app.example.com {
	log {
		output file /var/log/janus/access.json
		format janus
	}
	janus
}

Registry, data plane, and hub state live in pooled process state: Caddy config reloads preserve registrations and eligible hub WebSockets. A committed policy change can close connections on hosts where hub admission was disabled. Janus control state is memory-only — a restart empties the registry and tenants re-register. See Caddyfile.minimal for the operator-facing starting point, Caddyfile.example for the full capability walkthrough, and docs/ for the contracts.

This repository is a Go module. Caddy is a dependency, not a git submodule. The janus binary is built from cmd/janus, which compiles stock Caddy, this module, and the Route 53 DNS provider into one executable; the module also loads into any custom Caddy build like any other plugin.

License: Apache License 2.0 (same family as Caddy’s source).

What Janus is — and is not

Every capability in Janus has a famous neighbor; the mix has none. The novel contract is admission itself: the app announces itself to its own edge. A tenant POSTs its name and hosts to /1.0/apps, heartbeats, and publishes worker unix sockets — and with that one registration it has TLS and ACME, HTTP/1–3, host routing, health-aware least-conn balancing, edge-terminated WebSocket fan-out, and LAN presence, with zero per-app edge configuration. That is the router contract of a PaaS — the shape of Fly's proxy or Heroku's router — in one self-hosted binary, with the running app as the source of truth and heartbeat reaping as the garbage collector: a heartbeat-leased app that stops heartbeating ceases to exist at the edge. The nearest historical relative is Phusion Passenger, the app-aware web server — but Passenger manages processes for its supported languages and learns about apps from the web server's own config; Janus speaks a JSON API and learns about apps from the apps.

Each neighbor is better at being itself. The honest comparison:

Neighbor What it does better What Janus does instead
Traefik Provider ecosystem — routing derived from Docker labels, Kubernetes Ingress/CRD/Gateway API, Consul, Nomad, ECS — plus a deep middleware catalog and a community that dwarfs this module Apps register themselves over plain HTTP; no container runtime, orchestrator, or label convention required — a bare process on a unix socket is a first-class tenant
Pushpin Protocol range for realtime: HTTP streaming, long-polling, SSE, SockJS, WebSocket-over-HTTP against a stateless backend The same architectural instinct — connections held at the proxy, tenant on plain HTTP — plus registry integration (an app's hub lives and dies with its registration) and a validated per-frame directive grammar executed at the edge
Caddy Everything it already is: listeners, ACME, HTTP/1–3, the Caddyfile, the admin API, the module ecosystem — all of it remains available beside Janus in the same process A second axis of dynamism: Caddy's admin API pushes operator config; the Janus registry pulls state from running apps, and a registration never touches the config

Traefik answers "what is my orchestrator running?"; Janus answers "what is announcing itself to me right now?" — the second question needs no infrastructure underneath the app. The performance ledger holds every number with raw provenance — sustained hub fan-out is ~0.4M deliveries/s, roughly independent of room size, with zero socket drops across a config reload. Pushpin proved the edge-held-connection pattern at Fastly scale; the hub is that pattern folded into the registry. And Caddy is not a competitor at all: Janus is a Caddy module, and every stock directive works unchanged next to it.

Janus is not:

  • a reverse-proxy configuration language. There is no parallel grammar — capabilities are normal Caddyfile directives with legal values, defaults, and hard errors, same as stock Caddy.
  • a persistent store. Janus does not persist registry, session, or hub state; tenants re-register after a restart. Caddy still writes configured certificate storage and log outputs.
  • a container orchestrator. Tenants supervise their own worker processes; Janus routes to them. Janus's service commands manage the edge itself, and browse may launch bounded renderer subprocesses.
  • a service mesh. One edge, inward-facing unix sockets — no sidecars, no inter-service mTLS fabric, no traffic policy between tenants.

The same binary spans the whole distance: janus.local answering a phone on a bare LAN with no DNS and no client install, and a production edge with ACME certificates and HTTP/3 — the difference is only Caddyfile. And with rip-server as the tenant, the same app file that runs standalone on a laptop registers, heartbeats, and pools behind Janus in production, unchanged.

Requirements

  • A supported prebuilt platform, or the current stable Go release (go.dev/dl) for source builds
  • A Caddyfile that loads Janus; start with Caddyfile.minimal

Install Go (macOS, Homebrew)

brew update
brew install go          # or: brew upgrade go
go version               # confirm current stable

Install Go (official tarball)

Follow go.dev/doc/install. On macOS Apple Silicon, that is typically the darwin-arm64 archive or .pkg from go.dev/dl. Ensure $(go env GOPATH)/bin is on your PATH so tools installed with go install are available.

Capability order

Cold capabilities land in order. Each step stands alone before the next is added.

# Capability What it does Doc
1 ping Proves module load, TLS, site admission, cascade capability-ping
2 control Opens the /1.0 listeners (internal/local/public) capability-control
3 hub Per-app WebSocket fan-out terminated at the edge; tenants observe and steer over HTTP capability-hub
4 mdns LAN presence: janus.local + per-app .local names over multicast DNS, and the read-only status front door capability-mdns
5 auth URL-prefix gates for auth-less apps: shared users, per-gate allow lists, host-wide session, Remote-User strip-and-inject capability-auth
6 files Registered ordered roots, transparent precompressed sidecars, SPA shells, and directory-gated site hosts capability-files, precompressed extension
7 sendfile Always-on final upstream X-Sendfile transformation with validators, ranges, and streaming capability-sendfile
8 browse Navigable hot and cold roots, content-addressed themes, bounded extension renderers, and process leases capability-browse
9 access log JSON-compatible durable access log plus bounded app-scoped NDJSON streams on /1.0 capability-access-log
10 webtransport WebTransport (HTTP/3 over QUIC) on the edge's own UDP 443, relaying datagrams byte-exact to a config-fixed UDP target; auth-gate tickets capability-webtransport
make janus   # go build ./cmd/janus -> bin/janus

go test ./...
./test.sh    # capability-ordered acceptance groups, ending with access

cmd/janus is the binary's main package: stock Caddy, the Janus module, and the Route 53 DNS provider (DNS-01 wildcard certificates), compiled as one executable. go.mod pins all dependencies, including explicit security-sensitive overrides. janus version, -v, -V, and --version all report the Janus and Caddy versions; the service verbs (autostart, start, stop, restart, status) are Janus's, as are apps, logs, serve, mode, firewall, and passhash. Janus wraps Caddy's config and trust commands with service defaults; trust --export exports the local CA. Other available commands retain Caddy's behavior, except file-server defaults to :8080. upgrade, add-package, and remove-package are unavailable; rebuild Janus to change its compiled modules.

1. ping (data plane)

Trusted wildcard cert in certs/; DNS → 127.0.0.1; SNI picks the site. No control plane required.

./bin/janus run
curl -s https://foo.ripdev.io/ping          # catchall → pong
curl -s https://on.ripdev.io/ping           # explicit on → pong
curl -s -o /dev/null -w '%{http_code}\n' https://off.ripdev.io/ping
# → 404

On some systems binding :443 needs elevated privileges (sudo ./bin/janus run …). On current macOS it often works without sudo.

2. control (/1.0)

Same process. Loopback HTTP and a unix socket serve the control API.

curl -s http://127.0.0.1:7600/1.0
curl -s http://127.0.0.1:7600/1.0/health
curl -s --unix-socket run/janus.sock http://janus/1.0

3. hub

WebSocket upgrades on hub-enabled sites terminate at Janus; JSON directive frames fan out per app at the edge, so app reloads never drop a socket. The tenant registers a bridge to observe frames and steer, and publishes through the control plane.

curl -s http://127.0.0.1:7600/1.0/hub       # fan-out / bridge counters
curl -s -X POST -H 'Content-Type: application/json' \
  --data '{"@":["/lobby"],"news":{"v":1}}' \
  http://127.0.0.1:7600/1.0/apps/$APP_ID/hub/publish

4. mdns

Opt-in LAN presence: janus.local (and every registered single-label .local host) answers over multicast DNS with no DNS server or client install, and a plain-HTTP front door serves a read-only, self-contained status page — registry, worker health, heartbeat freshness, and hub counters, with socket paths redacted — and the trust page, http://janus.local/trust: the one-time setup that has a phone or another machine trust the edge's own CA, so every .local name it serves gets a real lock. The page detects the device, hands iOS a configuration profile (/trust/ca.mobileconfig) and everything else the root certificate (/trust/ca.crt), and watches for the moment trust lands (/trust/check over HTTPS) to move the device on. The front door's own name also answers over HTTPS. An optional canonical origin turns the page into a hand-off ramp to real HTTPS, with a built-in diagnostic for router DNS-rebinding filters. The announced name must answer on the HTTP port; where a janus site covers it, that site serves the front door, and where a site of your own covers it, your site owns the name.

curl -s --unix-socket ~/.local/state/janus/run/janus.sock http://janus/1.0/mdns          # advertiser state
curl -s --unix-socket ~/.local/state/janus/run/janus.sock http://janus/1.0/mdns/status   # the front door's snapshot
curl -s -H 'Host: janus.local' http://127.0.0.1/status.json
curl -s -H 'Host: janus.local' http://127.0.0.1/trust/ca.crt                                # the CA, as a phone fetches it

Service discovery follows janus mode: LAN mode advertises only the selected interface's selected private IPv4 address; localhost and WAN modes advertise no LAN names. Changing that selection withdraws the old records and announces the new address. Standalone Caddy configurations retain their configured interfaces and IPv4/IPv6 discovery.

5. auth

URL-prefix gates in front of tenant apps that have no login story of their own: define a shared users table and one or more gate <path> { … } allow lists (credentials minted by janus passhash). Each gate's login door is exact {prefix}auth. One host-wide session — sign in once, sign out once; a request under a gate proceeds only if the session user is on that gate's allow list. Longest prefix wins; paths outside every gate stay open. What passes a gate carries Remote-User: <name>; cookies and client Remote-User are stripped on every fall-through. Sessions live in memory: unchanged reloads keep eligible sessions, removing a user or host revokes its sessions after commit, and a restart signs everyone out. Admins observe and revoke over /1.0/auth.

./bin/janus passhash                        # mint a version-a passhash (password prompted, never argv)
curl -s http://127.0.0.1:7600/1.0/auth      # wall counters + session count
curl -s http://127.0.0.1:7600/1.0/auth/sessions

6. files

Apps register ordered static roots. Janus serves files and same-root precompressed sidecars, can fall back to an SPA shell, and can admit a host only when its requested site directory exists.

7. sendfile

An upstream can return X-Sendfile after authorizing a download. Janus validates the path and serves the file with range and conditional-request support; this response transformation is always available and has no configuration toggle.

8. browse

Hot registered roots and cold configured roots can expose navigable directory listings with embedded or custom themes, bounded renderers, and process leases.

9. access log

Each Janus site opts into Caddy access logging with format janus. Durable output is byte-equivalent to Caddy's JSON encoder with the same options. Registered-app requests also publish bounded NDJSON to operator streams; Caddy policy remains authoritative, so entries excluded before encoder invocation publish nothing.

curl -s http://127.0.0.1:7600/1.0/access
curl -sN "http://127.0.0.1:7600/1.0/apps/$APP_ID/access?after=0"

10. webtransport

A web page cannot send UDP. webtransport terminates WebTransport (HTTP/3 over QUIC) on a UDP 443 listener the edge owns, at the same addresses as the HTTPS servers, and relays each datagram, opaque and byte-exact, between a browser session and one UDP address fixed in the Caddyfile: one connected UDP socket per session, one datagram in and one out, drop rather than queue. The global block enables the listener; a route on an exact-host site names the path and the target. GET <path> over TCP answers the descriptor the page dials from; on a site with auth, the descriptor carries a 60 s single-use ticket for the signed-in user, which the relay requires. HTTP/3 stays off on the servers sharing the port (servers { protocols h1 h2 }).

{
	janus {
		webtransport
	}
}
lyte.trusthealth.com {
	janus {
		auth {
			user steve <passhash>
			gate / { steve }
		}
		browse { root /srv/lyte/www }
		webtransport /lyte udp/127.0.0.1:41151
	}
}
curl -s http://127.0.0.1:7600/1.0/webtransport

Build and run

From this repository:

make janus        # go build ./cmd/janus -> bin/janus
./bin/janus run

From anywhere, against a published version:

go install github.com/shreeve/janus/cmd/janus@v1.19.0

Janus also remains a plain Caddy module: builders that assemble their own Caddy (xcaddy or a custom main) add github.com/shreeve/janus like any other plugin.

janus help lists every command. Most are Caddy's, under the janus name: janus trust, janus adapt, janus fmt, and the rest behave as Caddy's reference documents them, and there is no separate caddy command on a Janus host. The service verbs below are Janus's own, and run, start, stop, reload, and validate default to the service Caddyfile.

Confirm the modules are linked:

./bin/janus list-modules | grep -E '^janus$|route53'

Running as a service

One Janus per host is the model, and the binary manages it with the same verbs as Harbor:

Verb What it does
janus autostart Installs the edge as a service and starts it: running now, at every login (as root: every boot), and again after a crash.
janus autostart off [stop] Removes the service; a running edge is left alone unless stop is given.
janus start / janus stop Start or stop that edge. A stop is clean, so it stays stopped until the next login or boot. Stopping a stopped edge is a quiet no-op.
janus restart Stop and start again, and always finish: under the autostart item it is the manager's restart (a bounded stop, SIGKILL on its timeout, then the start boot performs); a bare edge that ignores its stop is ended after 10 s. This is how an installed upgrade takes effect. The Caddyfile is validated first, so a broken one leaves the edge as it is.
janus reload Apply an edited Caddyfile in place, keeping every connection.
janus status [--json] Running or not, under what, which binary and version, whether the binary is newer than the running edge, how many apps are registered, whether this machine trusts the edge's CA and where a phone trusts it, and the exposure mode: scope, bind, firewall, and each front-door address with how its reach is enforced. Exit 3 when stopped; exit 1 when a process is there but no control plane answers, the state a stop that never finished leaves behind, which janus restart replaces.
janus serve [dir] [--name N] Open a directory through the running edge with the browse capability, at https://<name>.localhost/ here and https://<name>.local/ on the LAN in lan mode (mdns). One registration, heartbeats while it runs, removed on Ctrl-C; nothing new listens.
janus apps [--json] What is registered with the running edge: each app's hosts, what serves them (workers, a files root, a site directory), and its lease. Exit 3 when no edge answers.
janus logs [-n N] [-f] The edge's log, live: the last lines, then each line as it arrives, across roll-overs, until Ctrl-C. Piped, it prints the last lines and exits (-f follows anyway). --supervisor reads the supervisor's capture, where a start that failed before the log opened left its reason; that is also what prints when the edge has no log yet.
janus validate Check the service Caddyfile without touching the running edge.
janus trust [--export FILE] / janus untrust Install (or remove) the edge's own CA in this machine's trust stores, through the service edge's admin socket. --export writes the root certificate (PEM) instead, for another machine's trust store; phones take it from http://janus.local/trust.
janus mode [localhost|lan|wan] Show the exposure mode, or set it (below) and put the edge on it; the same mode again re-resolves it after the lan address or default route moved.
janus firewall Re-apply the host firewall rule the mode needs (root); after a macOS update rewrites /etc/pf.conf.

Run it as yourself on a machine you log in to. Run it as root (sudo janus autostart, with janus installed where root finds it: the installer puts it in /usr/local/bin when run as root) for a server: ports 80 and 443 with no setcap, up before anyone logs in, unaffected by logouts. One or the other on a host, never both, since two edges would contend for the same ports.

The seeded Caddyfile serves nothing of its own except the LAN's local names, *.local (announced by mdns), with the edge's own CA, which janus trust installs here and http://janus.local/trust installs on a phone, one certificate per registered name at its first handshake. This machine's own names, *.localhost, are a drop-in janus writes into the sites directory (sites/localhost.caddy) and leaves to you afterwards; being a drop-in, it survives a Caddyfile rendered by another tool, so janus serve works on any host. Both sites carry the seed's defaults for an app: the hub (bridge mode, same-origin pages), precompressed files, and an access log in the janus format beside the process log. Apps register hosts under both, and everything else is a drop-in *.caddy file of your own.

On macOS a user's janus is installed as an application bundle, ~/Applications/Janus.app, and the janus command on PATH is a symlink into it. The bundle is what macOS identifies: its row under Local Network reads "Janus" with the logo, and the service item runs the executable inside it. Upgrades swap in a complete new bundle and keep the same identifier and path, so the one row and its grant survive; janus restart re-registers the service item when an install moved the edge. Root keeps the bare binary in /usr/local/bin (a daemon is exempt from Local Network privacy). install.sh --uninstall removes the command and the bundle once janus autostart off stop has taken the item down. Linux installs the bare binary as before.

The supervisor is launchd on macOS and systemd on Linux. As a user the service is a login item (~/Library/LaunchAgents/janus.edge.plist, or a systemd user unit, for which autostart also enables lingering so the edge survives logout); as root it is a system service (/Library/LaunchDaemons, or /etc/systemd/system/janus.service). It runs janus run --config <Caddyfile> in the foreground, restarts it after a crash, and never after a clean exit.

On macOS the user-level service is subject to Local Network privacy (macOS 15 and later). A launchd agent is judged on its own, so a denied edge announces janus.local while every query for it, from this machine or a phone, goes unanswered, with nothing in its log: the kernel drops inbound multicast for that one process. The fix is System Settings → Privacy & Security → Local Network: turn on the row for the janus executable, then janus restart. The row carries the executable's code-signing identifier. install.sh, the release archives, and make install sign every janus binary as com.github.shreeve.janus, so the row keeps its name and its grant across upgrades (an unnamed Go binary would appear as a.out). Never re-sign an installed binary under another name: macOS then holds several identities for one build UUID and can strand the edge deaf under all of them. Replace the binary the way install.sh does, a new file renamed over the old one; a cp onto the existing file keeps an inode whose signature macOS has cached, and every later launch of it is killed. The full account and the one-pass setup for a new Mac are in docs/20260919-225500-macos-local-network-privacy.md. A root service (a launchd daemon) and a tool run from Terminal are exempt, so janus run from a shell always hears the LAN even when the agent does not. tccutil does not cover this list, and sudo cannot remove the policy file. Linux has no equivalent. A phone that opens http://janus.local/trust then takes three steps of its own: download the profile, install it under General → VPN & Device Management, and enable it under General → About → Certificate Trust Settings.

The service's files:

User Root
Caddyfile ~/.config/janus/Caddyfile /etc/janus/Caddyfile
Drop-in sites ~/.config/janus/sites/*.caddy /etc/janus/sites/*.caddy
Environment (optional) ~/.config/janus/env /etc/janus/env
Control and admin sockets, pidfile ~/.local/state/janus/run/ /var/lib/janus/run/
Process log ~/.local/state/janus/log/janus.log /var/log/janus/janus.log

$XDG_CONFIG_HOME and $XDG_STATE_HOME move the user's Caddyfile and state. autostart seeds the Caddyfile when there is none: the control plane on its unix socket, Caddy's admin API on a socket only this edge's user can reach, ping on every site, the process log on a rolling file, and an import of the drop-in sites directory. It validates the Caddyfile before installing anything, so a bad config never becomes a restart loop, and refuses a state directory deep enough to push the socket paths past the unix limit. A tool that owns a site writes one *.caddy file into the sites directory and runs janus reload; the Caddyfile itself stays the operator's. KEY=value lines in the env file are loaded by run, adapt, validate, and reload when they use the service config. Explicit --envfile paths replace that default. Caddy's syntax applies, including quoted multiline values. Existing process environment values win, even if empty; with multiple files the first value loaded wins. JANUS_BIND is reserved for the stored mode and is rejected in every env file. A reload changes configuration placeholders; changes needed by the running process itself require a restart.

start, stop, reload, and validate default to the service Caddyfile when it exists; run does too when the current directory has no Caddyfile of its own. stop, reload, and restart reach the edge through its admin socket, so they can never touch another Caddy on the host.

See the operator contract for the command and reload-survival matrices, environment precedence, and older-config migration.

Upgrading is the installer followed by a restart:

curl -fsSL https://raw.githubusercontent.com/shreeve/janus/main/install.sh | bash && janus restart

janus status says when the installed binary is newer than the edge that is running. On Linux a user's edge needs cap_net_bind_service on the binary to bind ports 80 and 443; autostart says so when it is missing, and the installer restores it across upgrades.

JANUS_SERVICE_LABEL renames the launchd item or systemd unit (default janus.edge and janus.service). With XDG_CONFIG_HOME, XDG_STATE_HOME, and XDG_DATA_HOME pointed elsewhere it lets a test suite run an edge beside the host's real one, with its own Caddy storage; that edge needs its own ports in its own Caddyfile.

Rip's rip sites registers apps with this edge and drops its site files into the sites directory; the edge itself is Janus's to run.

Exposure modes

The edge listens where its exposure mode says, on ports 80 and 443, IPv4 and IPv6 as separate sockets:

Mode Listens on Reachable by
localhost 127.0.0.1 and ::1 this machine only — the default
lan localhost plus one interface's private IPv4 address this machine and its local network; a private address is unreachable from the internet by addressing alone
wan every interface anyone the network lets through

janus autostart --scope lan installs with a mode, janus mode <scope> changes it later, and janus status reports it. lan uses the interface the default route leaves through, the one this host reaches its network by; --interface <name> pins another (--interface auto returns to the default route), and when the interface has several private addresses --v4 names the one to use — the address is never guessed among several. lan is IPv4 only: a public address is wan, and IPv6 (whose interface addresses are globally routable) stays on the loopback and on wan. Link-local addresses and tunnel, bridge, and VM interfaces are never candidates.

The mode is stored in scope.json under the state directory, and the service Caddyfile binds through default_bind {$JANUS_BIND}, which run, reload, validate, and adapt fill in from the stored mode; the mode is the one source of that value. The running edge checks its own sockets against the mode every few seconds and stops, loudly, rather than serve wider than the mode allows. The webtransport relay's UDP 443 is the one UDP listener the mode covers, at the same addresses as HTTPS; HTTP/3 stays off in the seed because it would bind the relay's port.

In wan the local name families are off: a request whose Host is <name>.local, <name>.localhost, localhost, via.rip, or <name>.via.rip (public DNS pins the via.rip names to 127.0.0.1) is refused (421) by every janus site, the front door included, no certificate is minted for such a name, mdns announces nothing, and janus serve says why it has nothing to open. Those names belong to a machine you sit at and a network you are on; a public address has neither, and neither the seed's local sites nor a via.rip drop-in need editing to be safe there. The mode governs the service edge; a Caddyfile run some other way is localhost by default and keeps its local names.

What enforces a mode differs by platform, and status says which:

  • macOS cannot bind an exact low port without root, so the socket is always the wildcard and a pf anchor (/etc/pf.anchors/janus, loaded from /etc/pf.conf ahead of Apple's anchors) scopes it to the loopbacks, plus the on-link subnet for lan. The rules are stateless, so narrowing the mode cuts the connections the wider mode admitted, and the block covers every destination, so an address the host gains later is covered too; the cost is that in localhost and lan the host also stops forwarding ports 80 and 443 for others (Internet Sharing, VMs on shared networking). pf is enabled by reference token, now and at boot by a small LaunchDaemon, since macOS loads pf rules at boot but leaves pf off. Loading the anchor is the one step that needs root, and it is the only thing root does: janus mode and autostart write their files as you and run sudo janus firewall when a terminal can prompt (without one they refuse, exit 4, before changing anything); sudo janus firewall and sudo janus status act on your edge when root has none of its own. The edge refuses to serve localhost or lan until the anchor is in place, so a wildcard socket is never open unscoped.
  • Linux binds the exact addresses (the low ports need cap_net_bind_service for a user's edge) and needs no firewall in any mode, since lan's address is private. The system unit never stops retrying: a lan edge whose DHCP address moved comes back the moment janus mode lan stores the new one.
  • Windows: exposure modes are not supported yet; the verbs refuse.

janus status never claims more than it verified. On macOS without root the firewall shows as missing when the anchor files are not in place and otherwise unverified (run: sudo janus status); with root it verifies that pf is enabled and holds exactly the anchor's rules. Each address says what enforces its reach: the loopbacks are host-enforced (loopback), a private address host-enforced (RFC1918) by addressing alone, and wan is network-gated (wildcard).

Prebuilt releases

On macOS and Linux, one command installs the latest release — it picks the right archive for the platform, verifies its sha256 against the published checksums, and installs janus into ~/.local/bin (as root: /usr/local/bin, so a system deploy keeps its path; override either with BIN=...; sudo only if the destination is root-owned):

curl -fsSL https://raw.githubusercontent.com/shreeve/janus/main/install.sh | bash

Then janus autostart makes it the host's edge (Running as a service).

Pin a version with ... | bash -s v1.19.0. Uninstall with ... | bash -s -- --uninstall — the binary goes; your Caddyfile, service units, and certificates stay.

The tagged release workflow publishes five self-contained archives:

Platform Archive
macOS Apple Silicon janus-<tag>-osx-arm64.tar.gz
Linux x86-64 janus-<tag>-linux-amd64.tar.gz
Linux ARM64 janus-<tag>-linux-arm64.tar.gz
Windows x86-64 janus-<tag>-windows-amd64.zip
Windows ARM64 janus-<tag>-windows-arm64.zip

Download the matching archive from the releases page and extract it. On macOS and Linux, run ./install.sh to install janus into ~/.local/bin (as root: /usr/local/bin), or choose another destination with BIN="$HOME/bin" ./install.sh. The extracted binary also runs in place. On Windows, run janus.exe directly. Each archive also contains Caddyfile.minimal, Caddyfile.example, the README, and the license; the installer deliberately leaves configuration in the archive rather than overwriting a live Caddyfile. The release's janus-<tag>-checksums.txt verifies every archive. (Debian packages the unrelated WebRTC gateway janus-gateway as janus; on a host running both, install this binary under a different BIN.)

What changed in each release is in CHANGELOG.md.

Release builds run on native GitHub runners and compile from the pushed tag, so janus version on a downloaded binary reports the exact Janus and Caddy versions, and janus build-info records the tagged module version. Pushing a v* tag runs the release workflow automatically.

For a local development build:

make janus              # working tree -> bin/janus
make unit               # fast Go test suite
make test               # build + unit + acceptance suite
make install            # build + install: ~/.local/bin as a user, /usr/local/bin as root
#                         (macOS: ~/Applications/Janus.app, with janus linked into it)
# make install BIN="$HOME/bin"

JSON config

The Caddyfile adapts to the following partial JSON shape. Site-scoped capabilities cascade global → site → built-in default. control and mdns are process-wide; access logging belongs to each HTTP site's log config; sendfile is always on and has no config key.

heartbeat_ttl defaults to 15s; JANUS_HEARTBEAT_TTL supplies the fallback when it is unset. The pooled registry captures this value when it is first provisioned. Changing it requires a restart: a config reload currently rejects a changed effective value with an error requiring a restart. GET /1.0 reports the running heartbeat_ttl; janus serve sends heartbeats every third of that interval (5s for older edges without it).

{
  "apps": {
    "janus": {
      "control": [{ "mode": "local" }],
      "ping": true,
      "hub": { "enabled": true, "path": "/hub", "max_conns": 4096 },
      "mdns": { "name": "janus.local" },
      "auth": { "enabled": true, "replace": true, "users": [{ "name": "alice", "credential": "a…" }], "gates": [{ "prefix": "/", "allow": ["alice"] }], "ttl": "8h" },
      "heartbeat_ttl": "15s"
    },
    "http": {
      "servers": {
        "srv0": {
          "listen": [":443"],
          "routes": [{
            "match": [{ "host": ["app.example.com"] }],
            "handle": [{ "handler": "janus" }]
          }]
        }
      }
    }
  }
}

Layout

Path Role
app.go Process-wide janus app (control, global defaults, pooled state)
handler.go Site http.handlers.janus (admission + site overrides)
caddyfile.go Caddyfile wiring: global janus block + site directive parsing, directive order
doc.go Package overview (the go doc face of the module)
state.go Pooled process state (registry, data plane, hubs survive reloads)
version.go The running version, published by the binary or read from build info
cascade.go Cascade helpers shared by every site-scoped capability
control.go Control listener config (control internal/local/public, token:…)
control_api.go Control listeners + /1.0 mux (meta, health, tls/ask)
control_hub.go Hub control surface (publish, snapshot, counters)
apps.go Hot apps registry (CRUD, upstreams, bridge, heartbeats, TTL sweep)
dataplane.go Host → worker-socket proxying (least-conn, health, marked 503s)
ring.go Doorbell ring: single-flight wake-up for dirty apps
hub.go Hub state and executor (membership, delivery, counters)
hub_frame.go Hub wire grammar (sigils, events, whole-frame validation)
hub_conn.go Hub connection lifecycle (writer, backpressure, close paths)
hub_ws.go Hub WebSocket edge (admission, upgrade, reader)
hub_bridge.go Hub tenant bridge (per-connection FIFO, open/text/close POSTs)
hub_config.go hub directive: parse, cascade, site table, floors
mdns.go mDNS advertiser (pooled, reconcile goroutine) + status front door
mdns_config.go mdns directive: parse, provision, validation
mdns.html Embedded status page (self-contained; zero external resources)
control_mdns.go mDNS control surface (GET /1.0/mdns)
auth.go Auth wall: gates, pooled sessions, throttle ladder, CSRF, login doors
auth_config.go auth directive: users, gates, parse, cascade, passhash codec, site table
auth_cmd.go janus passhash credential minter
auth.html Embedded login/status page (self-contained; zero external resources)
control_auth.go Auth control surface (GET /1.0/auth, session list + revocation)
access.go Pooled access bridge, registration sequence state, bounded event schema
access_encoder.go caddy.logging.encoders.janus, wrapping durable JSON
access_stream.go Access status and app-scoped NDJSON control streams
access_writer.go Response outcome observation with optional interfaces preserved
testkit/ Go test-support program: fixtures + WS driver for test.sh
bench/ Committed bench harness (baseline, leak probe, hub arm)
Caddyfile Working cold config (multi-site cascade demos)
Caddyfile.minimal Operator-facing starting point: one app site, one browsable root (validates standalone)
Caddyfile.example Production-shaped walkthrough of every capability and knob (validates standalone)
test.sh High-level acceptance suite (self-contained; not a substitute for go test)
docs/ Contracts, capability pages, measurements (YYYYMMDD-HHMMSS- prefixed; see docs/README.md)

Design notes

See docs/ for the control-plane sketch and related material. The /1.0 API follows an Incus-inspired style (envelopes, resource paths) while remaining Janus’s own protocol; writes carry no fencing fields — the tenant serializes its own writes (see the pool protocol).

Name

In Roman myth, Janus is the god of doorways and thresholds — beginnings, passages, and the space between inside and outside. He is shown with two faces: one looking out, one looking in. That is the shape of this module. One face serves the public world over TLS; the other coordinates private upstreams, registry, and control-plane state so that serving is possible. The passage between them is the product.

Documentation

Overview

Package janus is a Caddy module that fronts disposable worker pools: cold Caddyfile capabilities on the data plane and a hot /1.0 control API on the control plane. Its runtime coordination state is memory-only.

Janus registers four Caddy modules: the app "janus" (process-wide control listeners and capability defaults, configured in the global options block), the HTTP handler "http.handlers.janus" (per-site admission and capability overrides), "tls.permission.janus" (in-process on-demand certificate admission), and "caddy.logging.encoders.janus" (durable JSON access logging and live observation). Cold config admits capabilities; the hot registry wires tenants: apps register their hosts, publish their worker unix sockets, and heartbeat on /1.0, while Janus routes admitted requests host→upstream with doorbell-driven reloads that are invisible to clients.

Capabilities land in order: ping (1) proves the chassis, control (2) serves /1.0, hub (3) terminates WebSockets at the edge and fans JSON directive frames out per app while the tenant observes and steers over plain HTTP, mdns (4) advertises janus.local plus registered .local app hosts over multicast DNS and serves the read-only status front door, auth (5) is URL-prefix gates for auth-less apps: shared users, per-gate allow lists, one host-wide session, and Remote-User strip-and-inject on fall-through, and files (6) serves registered ordered roots and SPA shells with directory-gated site host patterns and trusted Rip-Site context. Sendfile (7) is an always-on reverse-proxy response protocol: a final application X-Sendfile instruction selects any regular file Janus can open, and Janus applies validators, ranges, and streaming at the edge. Browse (8) turns selected hot and cold roots into navigable spaces with a content-addressed theme and bounded extension renderers. Access log (9) wraps Caddy's JSON encoder without changing durable bytes and publishes bounded app-scoped NDJSON through the control plane. Webtransport (10) terminates WebTransport on the edge's own UDP 443 listener and relays each datagram, opaque and byte-exact, to a UDP target fixed per exact-host site; a gated site's descriptor mints the ticket that carries the auth wall's decision to the relay.

The registry, data plane, and hub state live in pooled process state (caddy.UsagePool), so Caddy config reloads preserve registrations and eligible hub sockets. Committed hub-policy changes and host removal can close affected sockets; registry DELETE, heartbeat TTL reap, or final pooled-state cleanup tears down the registration itself. A restart empties the registry and tenants re-register.

The authoritative contracts live under docs/: the phased build spec, one page per capability, the Janus↔tenant pool protocol, and the performance ledger with raw bench provenance.

Index

Constants

View Source
const (
	DefaultControlInternal = "run/janus.sock"
	DefaultControlLocal    = "http://127.0.0.1:7600/"
	DefaultControlPublic   = "https://0.0.0.0:7601/"
)

Default listen targets when a control line omits the address.

Variables

This section is empty.

Functions

func ExposureScope added in v1.14.0

func ExposureScope() string

ExposureScope is the published mode, "" when nothing published one.

func SetExposure added in v1.15.1

func SetExposure(scope, iface string, lanIPv4 netip.Addr)

SetExposure publishes the service's validated scope and LAN selection together, so discovery cannot combine a new mode with an old address.

func SetVersion added in v1.18.1

func SetVersion(v string)

SetVersion publishes the version the binary reports.

func Version added in v1.18.1

func Version() string

Version is the published version, else this module's version in the build info, else "dev".

Types

type AccessEncoder added in v1.5.0

type AccessEncoder struct {
	caddylogging.LogEncoderConfig
	// contains filtered or unexported fields
}

AccessEncoder preserves Caddy's JSON access log while publishing the bounded operator stream for entries carrying Janus request facts.

func (*AccessEncoder) AddArray added in v1.5.0

func (e *AccessEncoder) AddArray(key string, value zapcore.ArrayMarshaler) error

func (*AccessEncoder) AddBinary added in v1.5.0

func (e *AccessEncoder) AddBinary(key string, value []byte)

func (*AccessEncoder) AddBool added in v1.5.0

func (e *AccessEncoder) AddBool(key string, value bool)

func (*AccessEncoder) AddByteString added in v1.5.0

func (e *AccessEncoder) AddByteString(key string, value []byte)

func (*AccessEncoder) AddComplex64 added in v1.5.0

func (e *AccessEncoder) AddComplex64(key string, value complex64)

func (*AccessEncoder) AddComplex128 added in v1.5.0

func (e *AccessEncoder) AddComplex128(key string, value complex128)

func (*AccessEncoder) AddDuration added in v1.5.0

func (e *AccessEncoder) AddDuration(key string, value time.Duration)

func (*AccessEncoder) AddFloat32 added in v1.5.0

func (e *AccessEncoder) AddFloat32(key string, value float32)

func (*AccessEncoder) AddFloat64 added in v1.5.0

func (e *AccessEncoder) AddFloat64(key string, value float64)

func (*AccessEncoder) AddInt added in v1.5.0

func (e *AccessEncoder) AddInt(key string, value int)

func (*AccessEncoder) AddInt8 added in v1.5.0

func (e *AccessEncoder) AddInt8(key string, value int8)

func (*AccessEncoder) AddInt16 added in v1.5.0

func (e *AccessEncoder) AddInt16(key string, value int16)

func (*AccessEncoder) AddInt32 added in v1.5.0

func (e *AccessEncoder) AddInt32(key string, value int32)

func (*AccessEncoder) AddInt64 added in v1.5.0

func (e *AccessEncoder) AddInt64(key string, value int64)

func (*AccessEncoder) AddObject added in v1.5.0

func (e *AccessEncoder) AddObject(key string, marshaler zapcore.ObjectMarshaler) error

func (*AccessEncoder) AddReflected added in v1.5.0

func (e *AccessEncoder) AddReflected(key string, value any) error

func (*AccessEncoder) AddString added in v1.5.0

func (e *AccessEncoder) AddString(key, value string)

func (*AccessEncoder) AddTime added in v1.5.0

func (e *AccessEncoder) AddTime(key string, value time.Time)

func (*AccessEncoder) AddUint added in v1.5.0

func (e *AccessEncoder) AddUint(key string, value uint)

func (*AccessEncoder) AddUint8 added in v1.5.0

func (e *AccessEncoder) AddUint8(key string, value uint8)

func (*AccessEncoder) AddUint16 added in v1.5.0

func (e *AccessEncoder) AddUint16(key string, value uint16)

func (*AccessEncoder) AddUint32 added in v1.5.0

func (e *AccessEncoder) AddUint32(key string, value uint32)

func (*AccessEncoder) AddUint64 added in v1.5.0

func (e *AccessEncoder) AddUint64(key string, value uint64)

func (*AccessEncoder) AddUintptr added in v1.5.0

func (e *AccessEncoder) AddUintptr(key string, value uintptr)

func (AccessEncoder) CaddyModule added in v1.5.0

func (AccessEncoder) CaddyModule() caddy.ModuleInfo

func (*AccessEncoder) Cleanup added in v1.5.0

func (e *AccessEncoder) Cleanup() error

func (*AccessEncoder) Clone added in v1.5.0

func (e *AccessEncoder) Clone() zapcore.Encoder

func (*AccessEncoder) EncodeEntry added in v1.5.0

func (e *AccessEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error)

func (*AccessEncoder) OpenNamespace added in v1.5.0

func (e *AccessEncoder) OpenNamespace(key string)

func (*AccessEncoder) Provision added in v1.5.0

func (e *AccessEncoder) Provision(ctx caddy.Context) error

func (*AccessEncoder) UnmarshalCaddyfile added in v1.5.0

func (e *AccessEncoder) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

type App

type App struct {
	// Control is the exact set of control-plane listeners serving the
	// hot /1.0 API. Empty means one implicit internal (unix socket)
	// listener.
	Control []Control `json:"control,omitempty"`

	// Ping is the global default for the site-scoped ping capability.
	// Default: off. Sites may override.
	Ping *bool `json:"ping,omitempty"`

	// Hub is the global default for the site-scoped hub capability
	// (per-app WebSocket fan-out). Default: off. Sites may override.
	Hub *HubSettings `json:"hub,omitempty"`

	// Mdns is the process-wide LAN-presence capability: the advertised
	// .local identity, per-app .local advertising, and the plain-HTTP
	// front door. Default: off (nil).
	Mdns *MdnsSettings `json:"mdns,omitempty"`

	// Auth is the global default for the site-scoped auth wall (edge
	// authentication for auth-less apps): default posture plus the
	// default user set. Default: off. Sites may override.
	Auth *AuthSettings `json:"auth,omitempty"`

	// Files is the global default for the site-scoped registered-file
	// service. Default: off. Sites may override.
	Files *bool `json:"files,omitempty"`

	// FilesPrecompressed is the process-wide ordered set of sidecar
	// representations admitted by the global files capability. Site
	// blocks may turn files on or off but cannot retune this order.
	FilesPrecompressed []string `json:"files_precompressed,omitempty"`

	// Browse is the process-wide theme and renderer configuration.
	// Presence also enables the global site-scoped browse default.
	Browse *BrowseSettings `json:"browse,omitempty"`

	// HeartbeatTTL is how long a registered app may go without a
	// heartbeat before its registration is reaped (same effect as
	// DELETE). Default: 15s. The JANUS_HEARTBEAT_TTL environment
	// variable is honored as a fallback when this is unset.
	HeartbeatTTL caddy.Duration `json:"heartbeat_ttl,omitempty"`

	// Webtransport enables and tunes the process-wide WebTransport relay
	// listener (capability 10); routes are declared per site.
	Webtransport *WebtransportSettings `json:"webtransport,omitempty"`
	// contains filtered or unexported fields
}

App is the process-wide Janus application (cold config).

func (*App) CaddyModule

func (*App) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*App) Cleanup

func (a *App) Cleanup() error

Cleanup first tears down this generation's resources, including when Caddy rejects a started candidate without calling Stop. It then releases the app's reference on the pooled state; the last release (process shutdown) destructs it. A release that leaves other generations holding the state is either a successful reload's old generation retiring or an aborted reload's new generation being torn down — the advertiser tells them apart and rolls an aborted generation back to its surviving predecessor.

func (*App) Provision

func (a *App) Provision(ctx caddy.Context) error

Provision sets up the app. Registry, data plane, and hub state come from the pooled process holder, so a config reload binds the new app to the same live state instead of constructing a split-brain registry.

func (*App) Start

func (a *App) Start() error

Start starts the Janus app.

func (*App) Stop

func (a *App) Stop() error

Stop stops the Janus app. Pooled state (registry sweeper, hubs, open sockets, the mdns advertiser) deliberately survives: a config reload stops the old app while the new one is already serving the same pooled state.

func (*App) UnmarshalCaddyfile

func (a *App) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile parses the global janus block.

type AppRecord

type AppRecord struct {
	ID        string       `json:"id"`
	Name      string       `json:"name"`
	Hosts     []string     `json:"hosts"`
	Upstreams []Upstream   `json:"upstreams"`
	Site      *SitePolicy  `json:"site,omitempty"`
	Files     *FilesPolicy `json:"files,omitempty"`
	Lease     string       `json:"lease"`

	// Bridge is the tenant's hub bridge endpoint (optional; empty =
	// hub handshakes answer 503). Cold config never carries it: which URL
	// the tenant serves is tenant knowledge, exactly like socket paths.
	Bridge string `json:"bridge,omitempty"`
	// contains filtered or unexported fields
}

AppRecord is one registered app in the hot registry.

type AuthGate added in v1.3.0

type AuthGate struct {
	Prefix string   `json:"prefix"`
	Allow  []string `json:"allow"`
}

AuthGate is one path-prefix allow list after normalization.

type AuthSettings added in v1.2.0

type AuthSettings struct {
	// Enabled turns the wall on or off for the site. Default: off.
	// Sites may override the global default; explicit off beats an
	// inherited on. Bare auth / auth on sets Enabled true without
	// Replace — inherit the global users and gates.
	Enabled *bool `json:"enabled,omitempty"`

	// Replace is true when a non-empty auth { … } block was parsed.
	// Site Replace configs supply the entire effective auth config
	// (users, gates, ttl) and do not merge with global.
	Replace bool `json:"replace,omitempty"`

	// Users is this level's credential table.
	Users []AuthUser `json:"users,omitempty"`

	// Gates is this level's path-prefix allow lists.
	Gates []AuthGate `json:"gates,omitempty"`

	// TTL is the sliding idle session timeout. Default: 8h.
	TTL *caddy.Duration `json:"ttl,omitempty"`
}

AuthSettings configures the site-scoped auth wall. It appears in the global janus options (default posture) and per site (override). A non-empty auth { … } block at the site replaces the global config wholesale. The full contract is docs/20260728-160734-capability-auth.md.

type AuthUser added in v1.2.0

type AuthUser struct {
	Name       string `json:"name"`
	Credential string `json:"credential"`
}

AuthUser is one cold-configured credential: a lowercased, header-safe username and its passhash blob.

type BrowseEntry added in v1.4.0

type BrowseEntry struct {
	Name         string
	URL          string
	RawURL       string
	Kind         string
	Icon         string
	Size         int64
	SizeText     string
	Modified     time.Time
	ModifiedText string
	PreviewURL   string
	Rendered     bool
}
type BrowseLink struct {
	Name string
	URL  string
}

type BrowsePage added in v1.4.0

type BrowsePage struct {
	Version     int
	Title       string
	Path        string
	RootName    string
	AssetBase   string
	Parent      *BrowseLink
	Breadcrumbs []BrowseLink
	Entries     []BrowseEntry
}

type BrowseRenderer added in v1.4.0

type BrowseRenderer struct {
	Extension   string          `json:"extension"`
	Command     []string        `json:"command"`
	ContentType string          `json:"content_type"`
	Timeout     *caddy.Duration `json:"timeout,omitempty"`
	MaxOutput   *int64          `json:"max_output,omitempty"`
	Concurrency *int            `json:"concurrency,omitempty"`
}

type BrowseRoot added in v1.4.0

type BrowseRoot struct {
	Path  string `json:"path"`
	Cache string `json:"cache,omitempty"`
}

type BrowseSettings added in v1.4.0

type BrowseSettings struct {
	Theme       string           `json:"theme,omitempty"`
	Timeout     *caddy.Duration  `json:"timeout,omitempty"`
	MaxOutput   *int64           `json:"max_output,omitempty"`
	Concurrency *int             `json:"concurrency,omitempty"`
	Renderers   []BrowseRenderer `json:"renderers,omitempty"`
	// contains filtered or unexported fields
}

type BrowseSiteSettings added in v1.4.0

type BrowseSiteSettings struct {
	Enabled *bool        `json:"enabled,omitempty"`
	Roots   []BrowseRoot `json:"roots,omitempty"`
}

type Control

type Control struct {
	// Mode is internal, local, or public.
	Mode string `json:"mode,omitempty"`

	// Listen is a unix path (internal) or http(s) URL (local/public).
	Listen string `json:"listen,omitempty"`

	// Token is the raw token:… suffix value (no "token:" prefix), if any.
	Token string `json:"token,omitempty"`

	// TokenKind is env, file, or literal.
	TokenKind string `json:"token_kind,omitempty"`

	// CertFile and KeyFile are the TLS certificate/key paths from the
	// cert:… and key:… arguments. Both or neither; only meaningful on a
	// TLS listener (public mode or an https:// listen). Unset, a TLS
	// listener uses the committed dev pair certs/ripdev.io.{crt,key}.
	CertFile string `json:"cert_file,omitempty"`
	KeyFile  string `json:"key_file,omitempty"`
	// contains filtered or unexported fields
}

Control is one self-contained control-plane listener.

type FilesPolicy added in v1.3.0

type FilesPolicy struct {
	Roots      []FilesRoot `json:"roots"`
	ProxyFirst []string    `json:"proxy_first,omitempty"`
	Shell      string      `json:"shell,omitempty"`
}

FilesPolicy declares ordered static roots, worker-first prefixes, and the independent SPA shell.

type FilesRoot added in v1.3.0

type FilesRoot struct {
	Path   string `json:"path"`
	Cache  string `json:"cache,omitempty"`
	Browse bool   `json:"browse"`
}

FilesRoot is one ordered root with a finite cache policy.

func (*FilesRoot) UnmarshalJSON added in v1.4.0

func (r *FilesRoot) UnmarshalJSON(data []byte) error

type Handler

type Handler struct {
	// Ping overrides the global ping default for this site when non-nil.
	Ping *bool `json:"ping,omitempty"`

	// Hub overrides the global hub default/tuning for this site when
	// non-nil.
	Hub *HubSettings `json:"hub,omitempty"`

	// Auth overrides the global auth default for this site when non-nil.
	// A non-empty auth { … } block replaces the global config wholesale.
	Auth *AuthSettings `json:"auth,omitempty"`

	// Files overrides the global registered-file service default for this
	// site when non-nil.
	Files *bool `json:"files,omitempty"`

	// Browse overrides the global browse admission default and may carry
	// persistent cold roots for this exact-host site.
	Browse *BrowseSiteSettings `json:"browse,omitempty"`

	// Webtransport lists this exact-host site's relay routes (capability 10).
	Webtransport []*WebtransportRoute `json:"webtransport,omitempty"`
	// contains filtered or unexported fields
}

Handler is the site-level data-plane admission module.

func (Handler) CaddyModule

func (Handler) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*Handler) Provision

func (h *Handler) Provision(ctx caddy.Context) error

Provision sets up the handler.

func (*Handler) ServeHTTP

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error

ServeHTTP handles admitted requests: on the plain-HTTP port with the mdns front door in shared mode, the handler is the front-door decider (mine serves the front door, not-mine gets the redirect to HTTPS — never 421); everywhere else, site-scoped /ping answers first when enabled and everything else routes through the data plane (registry hosts → upstreams; unknown hosts → 404).

func (*Handler) UnmarshalCaddyfile

func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler.

janus
janus {
    ping
    ping off
}

type HubSettings

type HubSettings struct {
	// Enabled turns the hub on or off for the site. Default: off.
	// Sites may override the global default; explicit off beats an
	// inherited on.
	Enabled *bool `json:"enabled,omitempty"`

	// Mode selects tenant-observed bridge admission or edge-only direct
	// admission. Default: bridge.
	Mode *string `json:"mode,omitempty"`

	// Path is the WebSocket endpoint path; only upgrade requests to it
	// are intercepted. Must start with "/" and contain no "?" or "#".
	// Default: "/hub".
	Path *string `json:"path,omitempty"`

	// MaxConns caps concurrent hub connections per app. An app spanning
	// several hub-enabled hosts is admitted against the minimum effective
	// value across all of them. Default: 4096.
	MaxConns *int `json:"max_conns,omitempty"`

	// MaxFrame is the largest client frame, in bytes; larger frames close
	// the connection (1009). Minimum: 1024 (1KiB). Default: 65536 (64KiB).
	MaxFrame *int64 `json:"max_frame,omitempty"`

	// MaxChannels caps channels one connection may join. Default: 128.
	MaxChannels *int `json:"max_channels,omitempty"`

	// Origin is the browser Origin policy: "same" (Origin host must
	// equal the request Host), "any" (no check — for token-authenticated
	// non-browser clients), or "same" plus allowlisted hostnames, or
	// hostnames alone. Failure answers 403 before any tenant contact.
	// Default: same.
	Origin []string `json:"origin,omitempty"`
}

HubSettings configures the site-scoped hub: per-app WebSocket fan-out terminated at the edge. It appears in the global janus options (default) and per site (override); unset keys cascade from the global settings, then built-in defaults. The full contract is docs/20260720-162350-hub-design.md.

type MdnsSettings added in v1.1.0

type MdnsSettings struct {
	// Name is the advertised mDNS name: exactly one label plus ".local".
	// Default: "janus.local".
	Name string `json:"name,omitempty"`

	// Canonical is the https:// origin the front door hands off to
	// (client-side probe + redirect, diagnostic mode on failure).
	// Origin only — no path, query, fragment, or userinfo; never an IP
	// literal; never a .local name. Default: unset (plain dashboard).
	Canonical string `json:"canonical,omitempty"`

	// Interfaces pins advertising to exactly these interfaces. Default:
	// unset — the live multicast interface set with the loopback and
	// IPv4 link-local block list applied for standalone Caddy configs.
	// Service LAN mode uses only its selected interface and IPv4 address;
	// an explicit list must include that interface.
	Interfaces []string `json:"interface,omitempty"`

	// Apps controls per-app `.local` advertising. Default: on.
	Apps *bool `json:"apps,omitempty"`

	// Listen selects the front door's mode. Unset (the default) is
	// shared mode: the front door rides inside the HTTP app's plain-HTTP
	// port server behind a normal site block (http://*.local { janus })
	// and the janus site handler decides per request. Set ("[host]:port")
	// is dedicated mode: Janus opens its own listener at that address
	// with the strict Host allowlist.
	Listen string `json:"listen,omitempty"`
	// contains filtered or unexported fields
}

MdnsSettings configures the process-wide mdns capability: the advertised `.local` identity, per-app advertising, and the plain-HTTP front door. The full contract is docs/20260722-034619-capability-mdns.md.

type PermissionByJanus added in v1.13.0

type PermissionByJanus struct {
	// contains filtered or unexported fields
}

PermissionByJanus is Caddy's on-demand TLS permission, answered by the janus app in-process: a certificate may be minted for a host of a registered app or a cold browse claim, and for nothing else — the rule GET /1.0/tls/ask applies, with no listener for Caddy to dial and no round trip on a first handshake.

on_demand_tls {
	permission janus
}

func (PermissionByJanus) CaddyModule added in v1.13.0

func (PermissionByJanus) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*PermissionByJanus) CertificateAllowed added in v1.13.0

func (p *PermissionByJanus) CertificateAllowed(_ context.Context, name string) error

CertificateAllowed is the on-demand decision for one name. A nil return mints; any error denies.

func (*PermissionByJanus) Provision added in v1.13.0

func (p *PermissionByJanus) Provision(ctx caddy.Context) error

Provision binds the lookup to this config's janus app.

func (*PermissionByJanus) UnmarshalCaddyfile added in v1.13.0

func (p *PermissionByJanus) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile parses `permission janus`; it takes no arguments.

type SitePolicy added in v1.3.0

type SitePolicy struct {
	Host    string            `json:"host"`
	Dir     string            `json:"dir"`
	Aliases map[string]string `json:"aliases,omitempty"`
}

SitePolicy declares one directory-gated host pattern and its exact aliases.

type Upstream

type Upstream struct {
	// Path is the unix socket path Janus may dial.
	Path string `json:"path"`

	// Doorbell marks the tenant's wake-up socket. A doorbell entry must
	// be the only entry in the list. Phase 3 stores and validates the
	// flag; ringing is data-plane behavior (Phase 4).
	Doorbell bool `json:"doorbell,omitempty"`

	// Concurrency is the worker's own request cap — the same number its
	// gate enforces with a marked busy 503. Zero means unknown, and the
	// data plane learns the cap the old way, by bouncing. When published,
	// selection skips a socket whose in-flight count has reached it, so
	// a saturated worker is never dialed only to refuse. Meaningless on a
	// doorbell entry, which is never selected as a worker.
	Concurrency int `json:"concurrency,omitempty"`
}

Upstream is one entry in an app's upstream list.

type WebtransportRoute added in v1.19.0

type WebtransportRoute struct {
	// Path is the absolute, exact path a browser dials.
	Path string `json:"path"`
	// Target is the fixed UDP address, `udp/<ip>:<port>` with an IP literal.
	Target string `json:"target"`
	// Origin is the browser Origin policy: `same` (default) and/or hostnames;
	// `any` is not legal for a relay.
	Origin []string `json:"origin,omitempty"`
	// MaxSessions is the per-route session ceiling (default 4).
	MaxSessions *int `json:"max_sessions,omitempty"`
}

WebtransportRoute is one site route: `webtransport <path> <target> { … }`.

type WebtransportSettings added in v1.19.0

type WebtransportSettings struct {
	// MaxSessions is the process-wide session ceiling (default 256).
	MaxSessions *int `json:"max_sessions,omitempty"`
	// MaxDatagram is the largest payload relayed in either direction
	// (default and ceiling 1200 bytes).
	MaxDatagram *int `json:"max_datagram,omitempty"`
	// IdleTimeout is the QUIC max idle timeout (default 60s, minimum 30s).
	IdleTimeout caddy.Duration `json:"idle_timeout,omitempty"`
}

WebtransportSettings is the global `webtransport { … }` block. Presence enables the listener; every option has a default.

Directories

Path Synopsis
cmd
janus command
Command janus is the Janus edge binary: stock Caddy compiled together with the Janus module and the Route 53 DNS provider (DNS-01 wildcard issuance) as one executable named janus.
Command janus is the Janus edge binary: stock Caddy compiled together with the Janus module and the Route 53 DNS provider (DNS-01 wildcard issuance) as one executable named janus.
internal
strictjson
Package strictjson decodes one JSON value with no unknown struct fields or repeated object keys.
Package strictjson decodes one JSON value with no unknown struct fields or repeated object keys.
testkit is the acceptance-suite support binary for test.sh: fixture HTTP servers on unix sockets, an RFC 6455 WebSocket driver, and small shell utilities.
testkit is the acceptance-suite support binary for test.sh: fixture HTTP servers on unix sockets, an RFC 6455 WebSocket driver, and small shell utilities.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL