Help us shape the future of Go! Take the 2026 Go Developer Survey

proxyinterstitial

package
v1.3.104 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package proxyinterstitial pkg/proxyinterstitial/interstitial.go c4-app-web

A branded, self-contained "Building a route over skywire…" page that the native mesh proxies (dmsgweb / skynetweb resolving proxies, skysocks-client and -lite) serve to a browser while a route to the target is still being established — instead of a raw connection error or a mute hang.

It mirrors the wasm-visor's in-tab interstitial (pkg/wasmhv/browse-bootstrap.html) but is fully static: no SharedWorker, no streamed log. Both variants retry the same URL forever — a transient page at a fast cadence, a hard-failure page at a lazy one (it also offers a manual Retry). The retry probes with fetch() before it navigates, so an attempt that lands while the proxy process itself is restarting cannot replace the page with the browser's own proxy-error page.

Injection model: these proxies dial the target inside a SOCKS5 Dial callback (or an io.Copy tunnel). On a *transient* dial failure for a plaintext-HTTP (port 80) request, the proxy returns Conn() in place of the error. The SOCKS5 library then splices the browser to that conn: the browser's HTTP request is read and discarded, and the interstitial HTTP response is written back. Raw-TLS (443) requests can't be intercepted without a MITM CA, so callers pass those straight through (no interstitial).

Package proxyinterstitial pkg/proxyinterstitial/stream.go c4-app-web

Streaming (HTTP chunked-transfer) variant of the route interstitial. Instead of the one-shot Content-Length page + client-side meta-refresh (interstitial.go), this holds the browser connection OPEN and flushes a progressive-HTML chunk per REAL route-setup attempt as it happens, then — once the route is up — flushes a final chunk that reloads the page into the now-live content.

What "real" means here, precisely. The interstitial is minted AFTER a dial has already failed transiently; there is no in-flight route-setup to subscribe to at that point, and pkg/router exposes no per-hop / noise-handshake progress hook (DialOptions has no observer; even the mux-telemetry harness only synthesizes coarse "established/failed" events around its own DialRoutes call). So the streamer DRIVES a fresh attempt via a caller-supplied Probe and streams the real outcome of each attempt (StatusLine of the actual error, then success). That is the honest, coarse signal available today; the granular "2-hop via <pk> → noise handshake → route group up" lines the design sketches require a NEW router-side progress callback — see the package doc / the docs/proxy-status-and-interstitial.md "streaming seam" note. This file does not invent those events.

Graceful fallback: an HTTP/1.0 client (no reliable chunked/progressive render) is served the existing one-shot page instead.

Index

Constants

View Source
const MarkerHeader = "X-Skywire-Interstitial"

MarkerHeader labels every HTTP response THIS package synthesizes in-process — the waiting interstitial and the fall-through reload page. Both are minted by the local skysocks-client when it cannot reach the exit, yet on the wire they are indistinguishable from an ordinary relayed "HTTP/1.1 200 OK" with an HTML body. Anything that has to tell "the exit really relayed this" from "our own client answered for it" — the visor's end-to-end proxy-exit verification, for one — is otherwise fooled by the very page it triggered. The header makes the distinction explicit; see IsSyntheticResponse.

Variables

This section is empty.

Functions

func Conn

func Conn(target, detail, mechanism string, isError bool) net.Conn

Conn returns a net.Conn that serves the interstitial as a one-shot HTTP response. Reads yield the response bytes then EOF; writes (the browser's request) are discarded. LocalAddr/RemoteAddr return *net.TCPAddr so it can be handed straight to go-socks5, which unconditionally type-asserts the address to *net.TCPAddr when building its BND reply.

func DriveStream

func DriveStream(ctx context.Context, w io.Writer, cfg StreamConfig)

DriveStream renders the streaming interstitial to w: the opening shell, one progress line per real Probe attempt, and a closing chunk (reload-into-content on success, or an error + manual-retry on a hard failure / deadline). Exported so a caller that already streams over an http.ResponseWriter / io.Pipe (e.g. the browse-origin reverse proxy) can reuse the exact same rendering without the chunked-conn framing. w is written as raw HTML fragments; the HTTP layer (chunkWriter for the SOCKS path, the net/http server for the proxy path) frames and flushes them.

func IsSyntheticResponse

func IsSyntheticResponse(raw []byte) bool

IsSyntheticResponse reports whether raw HTTP response bytes were synthesized by this package rather than relayed from a real exit. It checks MarkerHeader first and falls back to sniffing the two page bodies for their distinctive markup, so a NEWER verifier still recognizes a page served by an OLDER skysocks-client build that predates the header. raw may be a prefix of the response (callers commonly read a bounded first chunk), so the sniff runs over whatever was supplied.

func IsTransient

func IsTransient(err error) bool

IsTransient classifies a dial error as a route-setup-in-progress condition (worth an auto-refreshing interstitial) versus a hard failure. The patterns mirror the transient signals the wasm interstitial keys on: dmsg 202 / delegated-server-not-ready, no-route, and generic dial/connect churn during multi-hop setup.

func Page

func Page(target, detail, mechanism string, isError bool) string

Page returns the full HTML document for the interstitial. target is the host the browser was trying to reach (shown verbatim, HTML-escaped); detail is an optional one-line status/error string; mechanism names the forwarding surface ("skysocks" / "dmsg" / "skynet") so the fetch step is specific rather than a generic "over skywire". When isError is true the page renders the hard-failure variant (error styling, manual retry button plus a slow auto-retry); otherwise it renders the transient variant (spinner + a fast auto-retry).

Both variants retry FOREVER, and both retry by PROBING rather than by navigating blind. A plain <meta http-equiv="refresh"> re-navigates the tab unconditionally, so if the refresh happens to land while the proxy process itself is restarting (the browser's SOCKS connection is refused for a few seconds) the browser replaces this page with its own "proxy server is refusing connections" error — which has no refresh of its own, so the retry chain is dead until a human reloads. That is the exact dead end operators hit across a `proxy stop` / `proxy start`. The retry script instead fetches the same URL in the background: a refused fetch merely schedules another attempt (with capped backoff) and leaves the page standing, and a fetch that comes back WITHOUT this package's marker header means the route is warm and the real content is there, so the page reloads exactly once, into it. The meta-refresh survives inside <noscript> as the no-JS fallback.

func ReloadPage

func ReloadPage() string

ReloadPage is the minimal fall-through page served once the exit is reachable again: it immediately re-requests the SAME URL (via location.replace so the interstitial is not left in history), with a meta-refresh=0 fallback for a no-JS browser. The re-request loads the real content over the now-live route.

func ServeSOCKS5

func ServeSOCKS5(conn net.Conn, detail, mechanism string, statusOverride func(host string) []byte, exitReachable func() bool) error

ServeSOCKS5 answers a not-yet-connected SOCKS5 client (e.g. a browser pointed at skysocks-client's :1080) with a branded interstitial when the mesh route to the exit is down. skysocks-client is a transparent tunnel — the SOCKS5 handshake normally rides end-to-end to the exit — so when the client can't open a stream it must speak just enough SOCKS5 itself to reach the point of writing an HTTP body: complete the greeting, accept the CONNECT request, then (only for a plaintext-HTTP target, port 80) serve the interstitial. HTTPS/other-port requests are declined so raw TLS is never corrupted. Best-effort and deadline-bounded so it never stalls the client's reconnect. Returns nil if it served a page, an error otherwise; the caller closes conn regardless.

statusOverride lets the caller answer a reserved in-process host (e.g. status.skysocks) itself instead of the interstitial: once the CONNECT target host is parsed, if statusOverride != nil and it returns a non-nil body for that host, the SOCKS5 reply is sent, the browser's request drained, and the body written verbatim as the HTTP response — the interstitial is skipped. The override is resolved BEFORE the HTTP-only port gate and the exit-reachability check, and applies regardless of port, so the reserved status page is never declined or shadowed — it stays reachable exactly when the exit is down, the moment the user most needs to see why. Pass nil for the plain interstitial-only behavior. The closure lives in the caller so this package stays free of any pkg/proxystatus dependency.

exitReachable is the symmetric fall-through: this function is minted on a dial failure, but that failure can be transient (a single stream open failing while the session is actually up). If exitReachable != nil and reports the exit IS reachable at serve time, the waiting "building a route" interstitial would just pin the browser on a spinner even though the route is live — so instead a tiny reload page is served that immediately re-requests the original URL, letting it fall through to the real content. Pass nil to always serve the interstitial.

func ServeSOCKS5Parsed added in v1.3.99

func ServeSOCKS5Parsed(conn net.Conn, host string, port int, detail, mechanism string, statusOverride func(host string) []byte, exitReachable func() bool) error

ServeSOCKS5Parsed is ServeSOCKS5 for a caller that has already read the browser's greeting and CONNECT request and answered method selection itself, with host and port the target that request named.

skysocks-client needs this: it reads the target BEFORE deciding whether an exit is involved at all, so by the time a route-down interstitial is called for, the handshake ServeSOCKS5 would do has already happened and re-reading it would consume the browser's HTTP request instead.

The deadline is the caller's to set here, since the caller is mid-exchange.

func ShouldServe

func ShouldServe(port string) bool

ShouldServe reports whether a failed dial to the given target port should be answered with a transient interstitial rather than surfacing the error. Only plaintext HTTP (port 80, or an empty/implicit port) qualifies: a raw TLS (443) or arbitrary non-HTTP stream can't carry an HTML page, and writing one would corrupt the tunnel. err is classified so genuinely permanent failures (bad hostname, refused-by-policy) can be surfaced as the hard variant by the caller if it chooses; see IsTransient.

func StatusLine

func StatusLine(err error) string

StatusLine turns a transient dial error into a short, human-facing status line for the interstitial's detail slot — so the page shows WHY it is waiting (a live-ish progress cue) instead of a blank card. It deliberately does not echo the raw Go error (yamux/socks internals, PKs) which is noise to a user; callers that want the raw error on the page can pass it as detail directly. The mapping mirrors the transient signals IsTransient keys on.

func StreamClose

func StreamClose(ok bool, detail string) string

StreamClose finishes the document. On ok it appends a "route up" line and a script that reloads the SAME URL — which now streams real content because the successful probe left the route warm. On failure it shows detail + a manual retry, and does NOT auto-reload.

func StreamConn

func StreamConn(ctx context.Context, cfg StreamConfig) net.Conn

StreamConn returns a net.Conn that serves a chunked, progressively-rendered interstitial driven by cfg.Probe, suitable for handing to go-socks5 as the tunnel conn (same injection model as Conn). Writes (the browser's request) are read to completion first; an HTTP/1.0 request falls back to the one-shot page.

func StreamOpen

func StreamOpen(target, _ string) string

StreamOpen is the opening HTML of a streaming interstitial: the branded card header and the (open) live progress list. Subsequent StreamStep fragments append <li> rows to it; StreamClose finishes the list and the document.

func StreamStep

func StreamStep(text, state string) string

StreamStep is one live progress line. state is "active" (in progress), "done" (completed) or "err" (failed). text is HTML-escaped.

Types

type Probe

type Probe func(ctx context.Context) error

Probe attempts to establish/verify the route to the interstitial's target. It returns nil when the route is ready (the browser should reload into live content), or an error while still warming. The streamer calls it on a loop (StreamConfig.Interval) until it succeeds, a non-transient error occurs, or StreamConfig.Deadline elapses. A successful Probe should leave the route warm (e.g. in the resolver's route pool) so the browser's reload hits real content.

type StreamConfig

type StreamConfig struct {
	Target    string // host shown on the page (HTML-escaped by the renderer)
	Mechanism string // "skysocks" / "dmsg" / "skynet" — brands the copy
	Probe     Probe  // drives + observes the real route-setup attempt
	Interval  time.Duration
	Deadline  time.Duration
}

StreamConfig configures a streaming interstitial.

Jump to

Keyboard shortcuts

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