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

skynetweb

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: 25 Imported by: 0

Documentation

Overview

Package skynetweb pkg/skynetweb/hostrewrite.go c4-app-skynet

HTTP/1.1 Host-header rewriter for the resolver SOCKS5 dial path.

Motivation

A visit to `https://example.com.<pk>.skynet/` resolves to the visor <pk>, but the browser sends `Host: example.com.<pk>.skynet` over the wire. When the backend is a vhost-capable HTTP server (Caddy, nginx, traefik) configured for `example.com`, that Host value doesn't match any of its sites and the request 404s.

This wrapper sits between the SOCKS5 splice's plaintext stream (post-TLS-termination if TLS MITM is enabled) and the raw skywire conn to the backend. It parses each HTTP/1.1 request the browser sends, rewrites Host to the configured value (e.g. `example.com`), then re-emits the request to the backend. The response direction is passthrough — Set-Cookie/Location rewriting is left for a future iteration and the limitation is called out in the docs.

Boundary: HTTP/1.1 only. h2c (plain HTTP/2) and SSE-with-binary- upgrade scenarios are not handled. Most reverse-proxy deployments terminate browser TLS upstream of the backend's plaintext face anyway, so HTTP/1.1 is the common case.

Package skynetweb pkg/skynetweb/resolverhost.go c4-app-skynet

Shared parser for resolver hostnames used by BOTH the skynet resolving proxy (.skynet) and the dmsg resolving proxy (.dmsg). It generalizes the existing "<vhost>.<pk>.<suffix>" form to carry an optional routing chain:

[<vhost>.]<r1>.<r2>…<rN>.<destPK>.<suffix>[:port]

The destination PK is the label immediately before the suffix (unchanged from the existing convention, so single-PK hostnames parse identically). Labels to its LEFT that are PK-shaped or TpID-shaped form the routing chain, returned in SOURCE order (route[0] nearest the source, route[len-1] nearest the destination). Everything to the left of the first label that is neither a PK nor a TpID is the vhost, returned for Host-header rewriting.

The suffix decides what the routing labels MEAN — the parser only extracts and classifies them:

  • ".dmsg": a single rendezvous dmsg SERVER (a PK) to dial the dest through.
  • ".skynet": the route to the destination, where each hop is either a visor PK (any/auto transport) or a specific transport ID (uuid).

Classification is purely by shape — a PK label is 53-char base32 or 66-char hex; a TpID label is a 32-char hex or 36-char dashed uuid. An ordinary vhost label ("magnetosphere", "net") matches none of these. The one ambiguity left out of scope: a vhost label that is itself exactly PK- or TpID-shaped.

Package skynetweb pkg/skynetweb/runtime.go c4-app-skynet a localhost SOCKS5 proxy that resolves `<pk>.skynet[:<port>]` hostnames by dialing the remote visor's skynet server over the skywire routing mesh and performing the skynet client handshake.

The SOCKS5 proxy dials skynet directly — no localhost HTTP bridge. The browser sends CONNECT, the proxy establishes a raw TCP tunnel through skynet, and bytes flow end-to-end.

The package deliberately does not import pkg/router — route establishment is the visor's concern, so callers inject a SkynetDialer that wraps their own dialing primitive. In practice that's the visor's router (via pkg/visor/embedded_skynetweb.go), but the interface also lets tests and alternative consumers substitute a mock.

Package skynetweb pkg/skynetweb/stats.go c4-app-skynet

Mirror of pkg/dmsgweb/stats.go. Structurally identical so the UI layer can treat both resolvers' stats uniformly. See dmsgweb/stats.go for design notes — duplicated rather than factored into a shared package because it's ~60 lines and avoids cross-package coupling between the two resolvers.

Index

Constants

View Source
const DefaultDomainSuffix = ".skynet"

DefaultDomainSuffix is the TLD treated as skynet addresses when Config.DomainSuffix is empty.

Variables

This section is empty.

Functions

func NewHostRewriteConn

func NewHostRewriteConn(backend net.Conn, targetHost string) net.Conn

NewHostRewriteConn wraps backend so writes are HTTP-parsed and have their Host header replaced with targetHost. targetHost must be non-empty; the caller decides whether to apply the wrapper based on the parsed URL. Exported so the dmsg resolver (pkg/dmsgweb) can reuse it for the same vhost handling.

func PerformHandshake

func PerformHandshake(conn net.Conn, port uint16) error

PerformHandshake runs the skynet client-side handshake on an already-established connection: reads the server's ready byte, sends ClientMsg{Port: port}, and returns an error if the server replies with one. Exported so visor-layer adapters can call it without re-implementing the protocol.

func PerformServiceHandshake

func PerformServiceHandshake(conn net.Conn, port uint16, service, host cipher.PubKey) error

PerformServiceHandshake is PerformHandshake for a service that host runs under the service's own key. It names the service, and accepts the stream only when host echoes it and shows the service's signature naming host.

func Run

func Run(ctx context.Context, log *logging.Logger, dialer SkynetDialer, cfg Config) error

Run starts the SOCKS5 proxy. Blocks until ctx is canceled. The dialer is called directly for every .skynet hostname — no localhost HTTP bridge is involved.

Types

type Config

type Config struct {
	// DomainSuffix is the TLD matched by the resolver (default ".skynet").
	DomainSuffix string
	// ProxyAddr is the host the SOCKS5 proxy listens on. Empty means loopback
	// ("127.0.0.1"). Set to "0.0.0.0" or a LAN IP to serve other devices on
	// the network (a `.skynet` gateway for a home router's LAN). Only bind a
	// non-loopback address on a trusted network.
	ProxyAddr string
	// ProxyPort is the SOCKS5 listener port.
	ProxyPort uint
	// UpstreamSOCKS forwards non-matching SOCKS5 CONNECTs to this
	// upstream (e.g. chain with skysocks-client for regular web
	// traffic).
	UpstreamSOCKS string
	// UpstreamRules send chosen domains to a different upstream, or
	// "direct", ahead of UpstreamSOCKS.
	UpstreamRules []proxyroute.Rule

	// Stats, when non-nil, is updated for every request.
	// Optional; no collection happens when nil.
	Stats *Stats

	// Requests, when non-nil, records every .skynet dial with the path the
	// dialer reports, for the status pages.
	Requests *proxystatus.RequestLog

	// TLSMITM enables on-the-fly TLS termination for browser
	// connections to <pk>.skynet on the TLSPort, using LeafMinter to
	// produce per-host leaf certs signed by a locally-installed CA.
	// When false the runtime behaves exactly as before — pure SOCKS5
	// byte splice. See pkg/skynetca for the trust model and the
	// security argument for why TLS is terminated locally rather than
	// end-to-end.
	TLSMITM bool
	// TLSPort selects the destination port treated as TLS for MITM.
	// Defaults to 443. Ignored when TLSMITM is false.
	TLSPort uint16
	// TLSUpstreamPort is the destination port a MITM'd TLS connection is
	// sent to once decrypted. Defaults to 80, where visors serve HTTP.
	TLSUpstreamPort uint16
	// LeafMinter mints per-host leaf certs. Required when TLSMITM
	// is true; ignored otherwise.
	LeafMinter skynetca.LeafMinter

	// LocalPK is this visor's public key, used to detect self-lookups for the
	// SelfLoopback short-circuit.
	LocalPK cipher.PubKey
	// SelfLoopback, when true (the default), serves a request whose destination
	// is LocalPK from the local service in-process via SelfDial, instead of
	// routing out over skynet back to ourselves. Set false to force the full
	// self-route path — e.g. to test self-transports (a legitimate use case).
	SelfLoopback bool
	// SelfDial returns an in-process connection to the local service registered
	// on the given port. Used only when SelfLoopback short-circuits a
	// self-destined request. Nil disables the short-circuit.
	SelfDial func(port uint16) (net.Conn, error)
	// Aliases maps a destination label to a PK (e.g. "skywire" -> LocalPK), so
	// "skywire.skynet" resolves exactly like "<pk>.skynet".
	Aliases map[string]cipher.PubKey

	// StatusProvider, when non-nil, enables the reserved in-process status hosts
	// served through this proxy — a read-only diagnostic page (logs +
	// route/transport events + per-leg mux view) for a surface. Nil disables the
	// status hosts. See pkg/proxystatus.
	StatusProvider proxystatus.Provider
	// StatusSurface scopes which reserved status host THIS proxy layer owns and
	// answers for (e.g. SurfaceSkynet → only http(s)://status.skynet/). A status
	// host matching a DIFFERENT surface is NOT served here — it falls through so
	// the request continues up the proxy chain to the layer that owns it. Empty
	// means "serve any matched surface" (the pre-scoping behavior), kept so
	// standalone runtimes and tests need no wiring. Ignored when StatusProvider is
	// nil.
	StatusSurface proxystatus.Surface

	// Publish, when non-nil, is called once with a handler that serves ONE
	// already-established connection exactly as the listener would, protocol
	// sniff included. The visor registers it as an in-process local service so
	// an app on this visor reaches this resolver over the data plane it already
	// holds, with no second SOCKS hop on localhost. The handler stays valid
	// until Run returns.
	Publish func(serve func(net.Conn))
}

Config configures a skynetweb runtime.

type RouteLabel

type RouteLabel struct {
	IsTpID bool
	PK     cipher.PubKey // valid when !IsTpID
	TpID   uuid.UUID     // valid when IsTpID
}

RouteLabel is one parsed routing-chain element: either a visor public key (auto/any transport) or a specific transport ID.

func ParseResolverHost

func ParseResolverHost(host, suffix string, aliases map[string]cipher.PubKey) (vhost string, route []RouteLabel, dest cipher.PubKey, port uint16, err error)

ParseResolverHost parses host into its vhost, routing chain, destination PK and port. suffix must start with "." (e.g. ".dmsg", ".skynet"). It errors when the host doesn't end in suffix or the destination label isn't a valid PK.

aliases maps a human-friendly destination label (e.g. "skywire") to a PK, so "skywire.dmsg" resolves exactly like "<pk>.dmsg". Nil/empty disables aliasing.

type SkynetDialer

type SkynetDialer interface {
	DialSkynet(ctx context.Context, remote cipher.PubKey, port uint16, route []RouteLabel) (net.Conn, error)
}

SkynetDialer establishes a raw TCP tunnel to (remote, port) over the skywire routing mesh. The returned net.Conn must already have the skynet server's ready byte consumed and the client request (ClientMsg) / server reply (ServerReply) handshake completed — the caller then just pipes bytes through it.

See pkg/visor/embedded_skynetweb.go for the visor-side adapter that fulfills this interface using router.DialRoutes + the handshake helper below (PerformHandshake).

route is an optional explicit source-route to the destination, parsed from the hostname (e.g. <hop>.<dest>.skynet). Each element is a visor PK (any/auto transport) or a specific transport ID. When empty the dialer picks the path itself (direct transport, else route-finder). A non-empty route is single-path: no mux, and the reverse path mirrors the forward path.

type Stats

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

Stats collects per-resolver counters. Safe for concurrent use. Nil receiver methods are no-ops so callers don't need nil-guards.

func NewStats

func NewStats() *Stats

NewStats returns a zero-valued Stats with its started_at clock set.

func (*Stats) RecordRequest

func (s *Stats) RecordRequest() func(err error)

RecordRequest — see dmsgweb.Stats.RecordRequest.

func (*Stats) Snapshot

func (s *Stats) Snapshot() StatsSnapshot

Snapshot returns a stable point-in-time copy.

type StatsSnapshot

type StatsSnapshot struct {
	StartedAt     time.Time  `json:"started_at,omitempty"`
	UptimeSec     int64      `json:"uptime_sec,omitempty"`
	TotalRequests uint64     `json:"total_requests"`
	Successful    uint64     `json:"successful"`
	Failed        uint64     `json:"failed"`
	Active        int64      `json:"active"`
	LastRequestAt *time.Time `json:"last_request_at,omitempty"`
	LastSuccessAt *time.Time `json:"last_success_at,omitempty"`
	LastFailureAt *time.Time `json:"last_failure_at,omitempty"`
	LastError     string     `json:"last_error,omitempty"`
}

StatsSnapshot is the read-only view.

Jump to

Keyboard shortcuts

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