tunnel

package
v0.2.10 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package tunnel implements reverse-dial tunneling between agent and hub.

Index

Constants

View Source
const CredentialHeader = "X-Railgrid-Agent-Credential"

CredentialHeader is the upgrade-response header the provider returns the enrolment bundle on. It mirrors tunnel.AgentCredentialHeader in the edges provider; the two ends are pinned to each other by TestAgentCredentialWireFormat.

View Source
const DefaultSvcPolicy = SvcPolicyWarn

DefaultSvcPolicy is the policy used when --svc-policy is not given. This release defaults to warn so existing Services that point off the loopback keep working while the agent logs what enforce would block; the next release flips the default to enforce.

Variables

This section is empty.

Functions

func ParseSvcAllowedCIDRs

func ParseSvcAllowedCIDRs(raw []string) ([]netip.Prefix, error)

ParseSvcAllowedCIDRs parses --svc-allow-cidr values (each a CIDR such as 192.168.1.0/24 or 10.0.0.5/32; a bare IP is treated as a /32 or /128).

func SplitBaseAndCluster deprecated

func SplitBaseAndCluster(hubURL string) (base, cluster string)

SplitBaseAndCluster splits a hub URL into the base URL (scheme+host only) and the kcp cluster name embedded in the path.

For "https://railgrid.localhost:9443/clusters/abc123" it returns:

base    = "https://railgrid.localhost:9443"
cluster = "abc123"

For "https://railgrid.localhost:9443" (no /clusters/ path segment) it returns:

base    = "https://railgrid.localhost:9443"
cluster = "default"

The base URL is always used when dialling the hub so that /services/agent-proxy/ is routed by the hub's own mux before reaching the kcp reverse-proxy.

Deprecated: use apiurl.SplitBaseAndCluster directly.

func StartProxyTunnel

func StartProxyTunnel(ctx context.Context, hubURL string, credentials *CredentialStore, edgeName string, resourceType string, downstream *rest.Config, tlsConfig *tls.Config, stateChannel chan bool, sshPort int, svc SvcProxyOptions, cluster string, onEnrolled func(Credential), extraHeaders http.Header)

StartProxyTunnel establishes a reverse tunnel to the hub server. It runs an exponential backoff retry loop to maintain the connection. tlsConfig controls TLS verification for the WebSocket connection to the hub. Pass nil to use a default (secure) TLS config; use InsecureSkipVerify only in development environments.

resourceType is "kubernetes" (Kubernetes cluster agent), "server" (Linux bare-metal / systemd host agent), or "macos" (service-only macOS host agent). It selects which resource the agent dials on the single `edges` provider via apiurl.ProviderAgentProxyURL.

cluster is the tenant workspace's kcp LOGICAL-CLUSTER ID (e.g. "2hx82dl9ncmepp5l"), never a workspace path: the agent-ingress route is the shared grammar, and provider-sdk/dataplane refuses a path-form cluster segment because the hub proxy will not serve one either. If empty, it is extracted from the token (for SA tokens) or defaults to "default".

credentials holds the agent's scoped identity and re-mints it on the reconnect path. It supplies the bearer for every connect attempt — the bootstrap join token until the agent has enrolled, the TTL'd credential afterwards — and adopts the enrolment bundle the provider returns on the upgrade response of a join-token connect.

A nil store is a caller that manages its own bearer (tests).

svc is the /svc proxy host policy (--svc-allow-cidr / --svc-policy); see SvcProxyOptions and newSvcProxyHandler.

Types

type Credential added in v0.2.7

type Credential struct {
	Token     string    `json:"token"`
	TokenType string    `json:"tokenType"`
	ExpiresAt time.Time `json:"expiresAt"`

	HubURL     string `json:"hubURL"`
	CACertData []byte `json:"caCertData,omitempty"`

	Provider  string `json:"provider"`
	ClusterID string `json:"clusterID"`
	Resource  string `json:"resource"`
	Name      string `json:"name"`

	RefreshPath        string `json:"refreshPath"`
	SSHCredentialsPath string `json:"sshCredentialsPath,omitempty"`
}

Credential is the enrolment bundle: the token plus everything needed to refresh it. Nothing about the route is compiled into the agent — the provider that serves it renders RefreshPath, so an agent follows a provider that moved without being rebuilt.

func DecodeCredential added in v0.2.7

func DecodeCredential(encoded string) (Credential, error)

DecodeCredential parses the base64(JSON) bundle from the upgrade response.

type CredentialStore added in v0.2.7

type CredentialStore struct {
	// Fallback is the bootstrap join token, used until a credential arrives.
	Fallback func() string
	// Persist is called with each new credential so the agent can survive a
	// restart. Nil keeps it in memory only.
	Persist func(Credential) error
	// TLSConfig is used for the refresh call. Nil uses the default.
	TLSConfig *tls.Config
	// HTTPClient overrides the transport (tests).
	HTTPClient *http.Client
	// contains filtered or unexported fields
}

CredentialStore holds the agent's current credential and re-mints it through the provider before it expires.

It starts empty: an enrolling agent has only its bootstrap join token, which lives outside this store (the fallback in Token). The first successful connect delivers a bundle, Adopt records it, and from then on the join token is neither needed nor valid.

Refresh happens on the reconnect path rather than from a timer, for the same reason provider-sdk's TokenSource refreshes lazily: an agent that has stopped reconnecting has stopped needing a credential, and its identity should be allowed to lapse and be collected.

func (*CredentialStore) Adopt added in v0.2.7

func (s *CredentialStore) Adopt(credential Credential) error

Adopt records a credential the provider just issued.

func (*CredentialStore) Current added in v0.2.7

func (s *CredentialStore) Current() (Credential, bool)

Current returns the credential in hand, and whether there is one.

func (*CredentialStore) EnsureFresh added in v0.2.7

func (s *CredentialStore) EnsureFresh(ctx context.Context) error

EnsureFresh re-mints when the credential is past 80% of its life. It is called before every connect attempt, so a reconnect loop keeps the credential alive on its own.

A failed refresh is not fatal while the current token is still valid: the next attempt tries again, and there is a fifth of the TTL of room. Once the token has actually expired there is nothing left to authenticate the refresh with, and the edge must be re-enrolled with a fresh join token — which is the honest consequence of a credential that expires, and is stated in docs/edges-agent-credentials.md.

func (*CredentialStore) PostSSHCredentials added in v0.2.7

func (s *CredentialStore) PostSSHCredentials(ctx context.Context, body SSHCredentials) error

PostSSHCredentials hands the host's SSH credentials to the provider through the gated ssh-credentials verb.

The agent used to write this Secret itself, which is why it held get/create/update on Secrets and get/create on Namespaces in the tenant workspace — the broadest grant an agent had, on a credential that never expired, and precisely the rule the hub's identity policy refuses to mint for anyone. Now the agent proves which edge it is (the two gates) and the provider does the write; nothing in this body names a destination.

func (*CredentialStore) Rejected added in v0.2.7

func (s *CredentialStore) Rejected() bool

Rejected records that the provider answered the last connect with 401 while the store presented the credential in hand. When a join token is also available the next Token() returns it, so an agent restarted with a fresh join token but a stale saved credential (a hub that was rebuilt, an edge deleted and recreated under the same name) re-enrols instead of retrying the dead credential forever. Without a join token there is nothing to fall back to and the call is a no-op. Returns whether the next attempt will use a different bearer than the last one.

func (*CredentialStore) Token added in v0.2.7

func (s *CredentialStore) Token() string

Token is the bearer for the next connect attempt: the join token until the agent has enrolled, the scoped identity afterwards.

type HandshakeError added in v0.2.7

type HandshakeError struct {
	StatusCode int
	Err        error
}

HandshakeError is a WebSocket upgrade the hub answered with a non-101 status. It carries the status so the reconnect loop can tell a refused bearer (401) from an intermediary failure (a proxy 403, a 502 with no origin behind it).

func (*HandshakeError) Error added in v0.2.7

func (e *HandshakeError) Error() string

func (*HandshakeError) Unwrap added in v0.2.7

func (e *HandshakeError) Unwrap() error

type RunnerAuthRequest added in v0.2.7

type RunnerAuthRequest struct {
	Addon string `json:"addon"`
}

RunnerAuthRequest names the add-on whose harness credential is wanted.

type RunnerRegistry added in v0.2.8

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

RunnerRegistry is a late-bound holder for the harness plane.

The tunnel starts BEFORE the plane does: on a join-token bootstrap the agent has no hub credential until the provider issues one, and the plane needs that credential to watch spec.harness. The registry lets the tunnel's handlers be wired once, at startup, and pick the plane up when it exists. Every method is safe on a nil receiver and on an unset source, which is what an agent with no harness plane at all looks like.

func (*RunnerRegistry) Services added in v0.2.8

func (r *RunnerRegistry) Services() []discovery.DiscoveredService

Services returns the running runners, or nothing.

func (*RunnerRegistry) Set added in v0.2.8

func (r *RunnerRegistry) Set(source RunnerSource)

Set installs the harness plane. Called once, when the plane starts.

func (*RunnerRegistry) Token added in v0.2.8

func (r *RunnerRegistry) Token(port int) (string, bool)

Token returns the bearer for a supervised runner port.

type RunnerSource added in v0.2.8

type RunnerSource interface {
	// Services is one entry per RUNNING runner, advertised to the provider's
	// discovery loop alongside the detector results.
	Services() []discovery.DiscoveredService
	// RunnerToken returns the bearer for a loopback port this agent supervises,
	// and false for every other port. That "false" is the SSRF-shaped half of
	// the guarantee: a Service aimed at some other local port can never borrow a
	// runner's identity.
	RunnerToken(port int) (string, bool)
}

RunnerSource is the harness plane as the tunnel sees it: what runners are running, and what bearer each one expects. The tunnel deliberately knows nothing else about harnesses.

type SSHCredentials added in v0.2.7

type SSHCredentials struct {
	Username   string `json:"username"`
	Password   string `json:"password,omitempty"`
	PrivateKey []byte `json:"privateKey,omitempty"`
	HostKey    string `json:"hostKey,omitempty"`
}

SSHCredentials is the ssh-credentials request body. It mirrors the provider's sshCredentialsRequest.

type SvcPolicy

type SvcPolicy string

SvcPolicy selects what the agent does when a /svc target fails the host checks (see SvcProxyOptions).

const (
	// SvcPolicyEnforce refuses a disallowed target with 403 and never dials it.
	SvcPolicyEnforce SvcPolicy = "enforce"
	// SvcPolicyWarn dials a disallowed target anyway but logs the denial and
	// stamps X-Railgrid-Svc-Policy: warn on the response, so operators can find
	// the Services that need --svc-allow-cidr before flipping to enforce.
	SvcPolicyWarn SvcPolicy = "warn"
	// SvcPolicyAllowAny disables the allow list entirely (loudly logged at
	// startup). Link-local, unspecified, multicast and the cloud metadata
	// endpoints in svcHardBlockedPrefixes stay blocked even here — those are
	// never overridable.
	SvcPolicyAllowAny SvcPolicy = "allow-any"
)

func ParseSvcPolicy

func ParseSvcPolicy(raw string) (SvcPolicy, error)

ParseSvcPolicy validates a --svc-policy value. Empty selects DefaultSvcPolicy.

type SvcProxyOptions

type SvcProxyOptions struct {
	// AllowedCIDRs are the literal-IP ranges (and resolved-hostname ranges)
	// the agent may dial besides loopback and, in kubernetes mode, cluster DNS.
	AllowedCIDRs []netip.Prefix
	// Policy decides what happens when a target is outside that set.
	Policy SvcPolicy
	// Runners is the agent's harness plane, or nil. It is consulted for two
	// things: the runner entries on /api/v1/services, and the bearer the proxy
	// injects when a target is one of this agent's own runner ports.
	Runners *RunnerRegistry
}

SvcProxyOptions is the operator-configured part of the /svc proxy policy, set from --svc-allow-cidr / --svc-policy (or RAILGRID_AGENT_SVC_ALLOW_CIDR / RAILGRID_AGENT_SVC_POLICY) and threaded from agent.Options down to the tunnel's remote server.

Jump to

Keyboard shortcuts

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