0074: Sites on a port Short Leash didn't start

On this page

Status: accepted

Context

Some apps run outside Short Leash: a container that publishes a port, a dev server started from your own terminal, a tool with its own web interface. They still want a .test name, HTTPS and a share link. Short Leash's proxy (0002) already forwards to ports, but only to the app servers and commands it starts itself. Forwarding to an app Short Leash didn't start touches network exposure and ownership: the target must stay on this computer (invariant 2), and Short Leash must not take over a program it doesn't own (0005).

Decision

  • A port on an existing site. SiteSettings.proxy_to is a port number on a site Short Leash already has, set from Site settings › Port, leash proxy 3000 or POST /v1/sites/{name}/proxy. It beats everything else that decides how the site is served (a command, Octane, detection, a document root). The site becomes Detected::Port (kind: "port"), with no runtime, Node or Octane details.
  • Loopback only. The setting stores a bare u16, so there's no host to get wrong. Pasted addresses are accepted only for 127.0.0.1, ::1 and localhost; any other host is refused with "Short Leash only sends requests to this computer: use 127.0.0.1, ::1 or localhost."
  • Both loopback addresses, raced. Recent Node and Vite servers bind localhost to ::1 only, so the proxy connects to 127.0.0.1:<port> and [::1]:<port> at once and uses whichever answers first. It remembers that address per port and tries it alone first next time, falling back to both when it stops answering. Each connect is bounded by defaults::PORT_CONNECT_TIMEOUT (1 s), so a refused connect, which takes about 2 s on Windows, never holds a request.
  • No loops. Short Leash refuses its own ports: HTTP, HTTPS and DNS, as configured and as listening now, and the share listener ("Port 80 is Short Leash's own. Use the port your app listens on."). A site given one by hand in config.toml gets the same refusal from the proxy instead of being forwarded. A request that comes back some other way, such as through an app that passes requests on, is caught by the x-leash-port-hop header: the proxy lists in it the name of each port site the request went through, and a request that reaches a port site already listed, or has gone through defaults::PORT_HOPS_MAX (8) of them, gets a 508 page that says so. One port site's app calling another through Short Leash is not a loop.
  • Short Leash doesn't own the app. Short Leash never starts, stops, restarts or kills the app on the port (invariant 3); it only forwards to it, and offers no Start for it. Setting a port stops a dev server Short Leash was running for that site. Stop and Start on a port site only stop and resume forwarding. Processes the person adds to the site, keep-running ones included, run as they do for any site, so someone who wants the app to start with the site can add its command as a process: that's their own command, not Short Leash taking over an app it didn't launch.
  • Status from a check. Every 5 seconds (defaults::PORT_CHECK_EVERY) a background task connects to the port of each port site that isn't stopped, on both addresses, for at most defaults::PORT_CHECK_TIMEOUT (300 ms) each. It tells the UI only when an answer changed. Setting a port checks it at once. When nothing answers, the site's lamp is the stopped ring with "Nothing answers on port 3000", Needs a look says to start the app that serves it, and the share note says to start that app to share the site. A request then gets an error page saying nothing answers on the port and to start what serves the site, without the process-and-log wording of sites Short Leash runs.
  • Shares as for any site. A shared port site goes through the share listener and the same access rules, IP allowlist and password, before anything reaches the app (invariant 10).

Consequences

  • Containers and other servers get .test names, HTTPS and shares without Short Leash managing them.
  • The app must speak plain HTTP on that port. Short Leash adds HTTPS in front; it doesn't forward to an HTTPS upstream.
  • Apps that check the Host header (a Vite dev server's server.allowedHosts) need the .test name allowed in their own settings.
  • A name needs a folder, because Changes, terminals and processes are built on a site's folder. For an app with no project, the docs say to link any folder first (leash link grafana). Names without a folder would need a new kind of site, and we left them out.
  • Each port site costs one loopback connect every 5 seconds while it isn't stopped. The lamp follows the app within about that long; between checks it shows the last answer.
  • Short Leash can't tell a crashed app from one that hasn't started yet, and there's no restart to offer.