config

package
v0.8.4 Latest Latest
Warning

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

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

Documentation

Overview

Package config provides YAML-based configuration for the qURL Connector. It loads a high-level YAML config and can generate the internal FRP runtime config used by the qURL Connector.

Index

Constants

View Source
const DefaultAdminAddr = "127.0.0.1"

DefaultAdminAddr is the loopback bind for FRP's admin API when an operator opts in. Hardcoded loopback is deliberate — the admin API has no remote-management use case in the Docker install, and a future YAML change that flipped this to 0.0.0.0 would silently expose the surface to any container peer.

View Source
const DefaultAdminPort = 7400

DefaultAdminPort is the TCP port FRP's admin API binds when enabled.

View Source
const DefaultAuditFilePath = "/var/log/layerv/qurl-connector/audit.log"

DefaultAuditFilePath is the developer command's audit-log path when AuditConfig.FilePath and QURL_AUDIT_FILE are both unset.

View Source
const EnvAuditFile = "QURL_AUDIT_FILE"

EnvAuditFile is the env var that overrides AuditConfig.FilePath at Load time. Pure runtime override (not persisted to YAML by Save) so an operator redirecting the sink can roll back to the YAML default by unsetting the env var. See applyEnvOverrides.

View Source
const MetaClientVersion = "client_version"

MetaClientVersion is the FRP Login.Metas key qurl-connector uses to report its build version to qURL tunnel server. The server's min-client-version kill switch reads this value at Login; keep it in sync with qURL tunnel server/internal/tunnelauth.MetaClientVersion.

View Source
const MetaQURLKnockToken = "qurl_knock_token" //nolint:gosec // G101: this is the Login.Metas key NAME (wire contract), not a credential value

MetaQURLKnockToken is the FRP Login.Metas key qurl-connector populates with the AC-issued knock token harvested from `ackMsg.ACTokens[resource_id]` after a successful NHP knock. Cross-repo wire contract — must match qURL tunnel server's tunnelauth.MetaQURLKnockToken constant. The server passes the value to nhp-server's `POST /nhp/internal/token/validate` to verify that the inbound FRP login is preceded by a valid, recent knock from the same `RunID`.

gosec G101 fires on the literal "qurl_knock_token" — gosec classifies the substring as a credential pattern. This is the wire-contract field NAME, not a credential value baked into source. Renaming on either side without coordinating the other locks every knock-required client out with a `knock_invalid` Login reject. The key is pinned in contracts/qrts_knock_token_login_wire_contract.json and bound to this constant by pkg/share's TestQRTSKnockTokenLoginContract.

View Source
const MetaResourceID = "resource_id" //nolint:gosec // G101: this is the per-proxy Metas key NAME (wire contract), not a credential value

MetaResourceID is the FRP Login.Metas key qurl-connector populates per-proxy with the qURL resource_id. Cross-repo wire contract: must match qURL tunnel server's `tunnelauth.metaResourceID` ("resource_id"). Renaming on either side without updating the other strands every managed route with `resource_not_found`; qRTS requires this metadata and never substitutes SubDomain because that carries the independent routing identity.

gosec G101 fires on the literal "resource_id" because it looks like a credential field name; this is the wire-contract field NAME, not a credential. Same rationale as MetaQURLKnockToken above.

View Source
const UserConfigDir = ".config/qurl"

UserConfigDir is the home-relative directory holding qURL's user config (qurl-proxy.yaml) and the optional developer token file (~/.config/qurl/token). Exported so cmd/frpc can resolve the token path against the same constant rather than re-hardcoding ".config/qurl". Join with os.UserHomeDir(), NOT os.UserConfigDir() (which is ~/Library/Application Support on macOS).

Variables

View Source
var ErrConfigContinuityLockMissing = errors.New("config continuity lock is missing")

ErrConfigContinuityLockMissing reports an existing config whose canonical transaction lock is absent. Mutating callers must fail closed. Read-only callers may separately prove and retain an immutable snapshot without recreating the missing lock.

Functions

func AdminBindLooksRoutable

func AdminBindLooksRoutable(cfg *Config) bool

AdminBindLooksRoutable reports whether cfg.Admin.Addr would bind to a non-loopback address that's reachable from off-host. The result is the defense-in-depth check the warning sites in cmd/frpc rely on — they emit a stderr warning only when the listener is about to bind (i.e., from `run`), not on every config Load. Without this split, a scripted poller running `status --json` on a 5-second interval would spam stderr forever for a single misconfigured `admin.addr`.

Recognized loopback forms:

  • IP literal whose `net.IP.IsLoopback()` is true (`127.0.0.1`, `::1`, the whole `127.0.0.0/8` block).
  • The literal string "localhost" (case-insensitive) — common in operator configs even though it requires DNS to actually resolve. We don't issue a lookup here (no network I/O in config land) but the literal is unambiguous enough to treat as loopback.

All other hostnames (`host.docker.internal`, public DNS names, internal-only DNS names) return TRUE — treated as routable so Validate requires `allow_remote`. The operator has deviated from the IP-literal default, which is itself a deliberate change worth gating. The previous behavior (hostnames bypass the check) was a gap that contradicted the PR framing "no off-host reachability without two deliberate YAML changes" — a hostname resolving to a public IP at runtime would pass Validate but expose the surface off-host. Tightening here closes that gap; the carve-out for "localhost" remains because it's structurally ambiguous-but- always-local in every reasonable resolver config.

func AdminURL

func AdminURL(addr string, port int) string

AdminURL builds the FRP admin API base URL for (addr, port). Lives next to DefaultAdminAddr/DefaultAdminPort so the URL-construction contract (scheme=http, brackets-IPv6-via-net.JoinHostPort) is in the same package as the bind defaults. The 4 callers in cmd/frpc (run banner, add reload, status probe, status display) share this one source of truth.

net.JoinHostPort is the load-bearing detail: an operator setting `admin.addr: "::1"` would otherwise produce the invalid URL `http://::1:7400/...` and a silent connection failure.

func CanonicalPath

func CanonicalPath(path string) (string, error)

CanonicalPath resolves an existing config symlink to its target. For a new config, it resolves the longest existing ancestor and preserves the missing suffix so aliases cannot acquire different lock namespaces.

func Discover

func Discover(configFlag string) (string, error)

Discover locates the configuration file to use. It checks, in order:

  1. The explicit path from configFlag (error if not found).
  2. ./qurl-proxy.yaml in the current working directory.
  3. <binary_dir>/etc/qurl-proxy.yaml alongside the running binary.
  4. ~/.config/qurl/qurl-proxy.yaml in the user's home directory.

If no file is found, an error is returned. There is intentionally no fallback to a TOML config — qurl-connector is a brand-new product with no installed base predating the YAML schema; every install starts from `qurl-proxy.yaml`.

func ExecutableDir

func ExecutableDir() (string, error)

ExecutableDir returns the directory containing the running binary, resolving any symlinks.

func FRPProxyName

func FRPProxyName(routeID, replicaDiscriminator string) string

FRPProxyName renders the FRP proxy name for a route by joining the route's stable ID with the normalized per-replica discriminator. Empty discriminator returns the raw route ID. The runtime resolver normally supplies a discriminator at boot, including single-replica deploys, so this empty branch primarily preserves direct config-generation callers and older upgrade windows.

Wire contract (read-on-FRP-bump): the name must be unique per `(server_instance, group)` tuple but NOT globally unique across the FRP server — two routes with the same Name on DIFFERENT LoadBalancerGroups would route correctly. The salt covers the only collision shape we hit in practice (same group, same route.ID, different replica). See buildHTTPProxy's header comment for the group/auth rationale.

func GenerateFRPClientConfig

func GenerateFRPClientConfig(cfg *Config, machineID string) (*v1.ClientCommonConfig, []v1.ProxyConfigurer, []v1.VisitorConfigurer, error)

GenerateFRPClientConfig converts a qURL Config into the FRP v1 types that can be passed directly to client.NewService(). The machineID is injected into any subdomain template containing {{ .MachineID }}.

func LocalAdminURL

func LocalAdminURL(addr string, port int) string

LocalAdminURL is the AdminURL variant for callers reaching the FRP admin API FROM the same host (the `add` reload path and the `status` probe — both run alongside the daemon). Substitutes a loopback address for the unspecified bind addresses (`0.0.0.0`, `::`) so the outbound dial works on every OS, then defers to AdminURL for the URL construction.

Cross-platform correctness: Linux's kernel routing treats `0.0.0.0` as `127.0.0.1` on outbound dials, but Windows and macOS do not. An operator who set `admin.addr: 0.0.0.0` + `allow_remote: true` on Windows would otherwise see `add` and `status` silently fail to reach the local listener — the daemon IS reachable off-host (which is what they opted into), they just can't dial it from the same machine via the wildcard address.

`0.0.0.0` → `127.0.0.1`, `::` → `::1`. Loopback (`127.0.0.0/8`, `::1`), `localhost`, and non-wildcard addresses pass through unchanged.

func OpenImmutableConfigSnapshot

func OpenImmutableConfigSnapshot(path string) (_ *Config, snapshot *ImmutableConfigSnapshot, retErr error)

OpenImmutableConfigSnapshot securely reads one immutable customer config. Every ancestor must be root/euid-owned and protected from replacement by unrelated users; the final parent and file must additionally be non-group/other-writable. The final file must be a single-link regular file.

func Save

func Save(cfg *Config, path string) error

Save marshals cfg and atomically writes it through a pinned, locked config namespace. Parent directories are durably created if they do not exist.

func Validate

func Validate(cfg *Config) error

Validate checks a fully resolved cfg for structural correctness and returns a combined error listing every violation found. Managed routes must already carry their server-issued ConnectorRoutingID. Load uses the less-strict startup-input phase so a pinned ResourceID can still be hydrated from the qURL control plane before this final validation boundary.

func ValidateConnectorRoutingID

func ValidateConnectorRoutingID(s string) error

ValidateConnectorRoutingID reports whether s is the exact canonical routing label shape returned by the qURL control plane. Decoding validates the producer's lowercase, unpadded RFC 4648 base32 wire format; exact re-encoding rejects non-zero trailing bits. This validates an opaque value and never derives it from resource_id. Producer source of truth: the qURL control plane's ConnectorRoutingIDPrefix, ConnectorRoutingIDLength, and DeriveConnectorRoutingID.

func ValidateManagedRouteIdentities

func ValidateManagedRouteIdentities(routes []Route) error

ValidateManagedRouteIdentities enforces a one-to-one mapping between local managed routes, public resource identities, and producer routing labels. Multiple different routing labels on one Connector session are supported; aliasing either side is not. Duplicate public IDs would point one protected resource at multiple local targets, while a routing-label collision would put distinct resources into one FRP vhost/group and fan requests across them.

This is exported because run and add must check producer-returned identities before persisting the local graph. Callers may pass incomplete startup routes: empty identity fields are ignored here and validated by their phase-specific boundary.

func ValidateSlug

func ValidateSlug(s string) error

ValidateSlug reports whether s is an acceptable Connector resource slug per the qURL control-plane contract. Empty input is rejected — callers that want to allow empty (e.g. an optional YAML field) must short-circuit before calling.

func WithFileTransaction

func WithFileTransaction(path string, fn func(*FileTransaction) error) (retErr error)

WithFileTransaction runs fn while path's canonical transaction lock is held. The callback must use tx.Load/tx.Save; recursively calling top-level Save or another Acquire is unsupported and waits only until its context expires.

func WithFileTransactionContext

func WithFileTransactionContext(ctx context.Context, path string, fn func(*FileTransaction) error) (retErr error)

WithFileTransactionContext is WithFileTransaction with caller cancellation.

Types

type AdminConfig

type AdminConfig struct {
	Enabled     bool   `yaml:"enabled"`
	Addr        string `yaml:"addr,omitempty"`
	Port        int    `yaml:"port,omitempty"`
	AllowRemote bool   `yaml:"allow_remote,omitempty"`
	// Password IS a secret by design — gosec G117 flags any exported
	// struct field whose name matches a secret pattern, but this
	// field exists exactly to carry the FRP admin basic-auth
	// credential from YAML into run.go (where it's set on
	// common.WebServer.Password). The YAML file holding it is
	// expected to be 0600 by the surrounding install story; the
	// struct doesn't get serialized to any log/wire path. Renaming
	// to obscure intent would hurt readability; a custom redact-on-
	// print type is a future hardening (issue #190 obsoletes the
	// surface entirely). Per-line suppression with rationale per
	// repo policy on lint dodges.
	Password string `yaml:"password,omitempty"` //nolint:gosec // G117 - field name "Password" matches secret pattern by design; see comment above
}

AdminConfig gates FRP's built-in HTTP admin API (status + live reload). Default is OFF: the Docker install has no in-container caller for it, and shipping it always-on leaves an authenticated-but-machine-id-keyed surface that defeats the "every reachable path goes through NHP" posture. Operators must opt in explicitly.

Addr/Port have non-zero defaults applied in applyDefaults so an operator who opts in only needs to set Enabled.

AllowRemote is a SECOND opt-in required when Addr is non-loopback (`0.0.0.0`, a RFC1918 IP, a public IP, etc). Without it, Validate rejects the config — a single misedited Addr can't silently expose the admin API to any peer that can route to the host. The whole point of the gate is "no off-host reachability without two deliberate YAML changes."

Password is the FRP admin API basic-auth password. When unset, the admin listener may use getMachineID() as a loopback-only fallback, but only if the runtime can resolve a real protected machine ID. Minimal containers often cannot, and the daemon fails closed rather than accepting the sentinel "unknown" password. AllowRemote=true (off-host exposure) REQUIRES an explicit Password — Validate rejects the config otherwise. The machineID fallback is host-stable and partly inferable; allowing it on an off-host listener would mean a remote attacker who can guess the machineID gets admin access. Requiring an explicit password forces operators opting into off-host to provide real credentials. #190 tracks dropping admin API entirely in favor of stdio-pipe IPC, which obsoletes this entire surface; until then this gate closes the obvious hole.

type AuditConfig

type AuditConfig struct {
	// Enabled defaults to true. Set false to disable audit emission
	// entirely (the NopLogger is wired into the call sites).
	Enabled *bool `yaml:"enabled,omitempty"`

	// FilePath is the active audit-log file. Defaults to
	// DefaultAuditFilePath. The QURL_AUDIT_FILE env var overrides the
	// YAML (applied in applyEnvOverrides) so a Docker operator can
	// redirect the sink without rewriting the YAML.
	FilePath string `yaml:"file_path,omitempty"`

	// MirrorSlog defaults to true — every audit entry is mirrored
	// through slog.Default() at INFO level so central log shippers
	// (journald, CloudWatch, GCP Logging) see the same stream as the
	// file. Set false to disable when the file is the sole sink (eg
	// bind-mounted to an out-of-process shipper).
	MirrorSlog *bool `yaml:"mirror_slog,omitempty"`

	// BufferSize is the in-process entry channel buffer. Zero falls
	// through to pkg/audit's default (4096). Operators should rarely
	// need to tune this — the channel is sized for the burst rate of
	// a saturated control-plane path, not the steady-state rate.
	BufferSize int `yaml:"buffer_size,omitempty"`

	// MaxSizeMB is the active-file size threshold (in MB) above which
	// lumberjack rotates. Zero falls through to audit.DefaultMaxSizeMB
	// (100). See pkg/audit/rotation.go for the rationale.
	MaxSizeMB int `yaml:"max_size_mb,omitempty"`

	// MaxAgeDays evicts rotated backups older than this many days.
	// Zero falls through to audit.DefaultMaxAgeDays (90 — matches
	// SOC 2 / PCI DSS minimum hot-tier retention).
	MaxAgeDays int `yaml:"max_age_days,omitempty"`

	// MaxBackups caps the number of rotated backups retained. Zero
	// falls through to audit.DefaultMaxBackups (14 — ~1.4 GB on disk
	// at the default MaxSizeMB).
	MaxBackups int `yaml:"max_backups,omitempty"`

	// Compress gzips rotated backups when true. Defaults to true
	// (audit.DefaultCompress) — JSONL compresses 5-10× and lumberjack
	// runs gzip in a background goroutine so steady-state latency is
	// unaffected. Set false to opt out.
	Compress *bool `yaml:"compress,omitempty"`
}

AuditConfig configures the audit-log sink (file + slog mirror) and rotation policy. Defaults are production-safe — leaving the block out of qurl-proxy.yaml entirely produces a working audit pipeline against the default file path with the standard rotation knobs. See pkg/audit's LoggerConfig / RotationConfig for the underlying contract.

type Config

type Config struct {
	Server  ServerConfig  `yaml:"server"`
	NHP     NHPConfig     `yaml:"nhp"`
	QURL    QURLConfig    `yaml:"qurl"`
	Admin   AdminConfig   `yaml:"admin,omitempty"`
	Audit   AuditConfig   `yaml:"audit,omitempty"`
	Routes  []Route       `yaml:"routes"`
	Runtime RuntimeConfig `yaml:"-"`
}

Config is the top-level qURL proxy configuration.

func Load

func Load(path string) (*Config, error)

Load reads a YAML configuration file from path, resolves environment variable placeholders, validates the result, and applies defaults.

func NewDefaulted

func NewDefaulted() *Config

NewDefaulted returns an empty Config with the same defaults that Load applies to a parsed YAML file. Use this on the fresh-config path (when no YAML exists yet) so that the eventual Save writes a fully-populated file rather than a sparse one missing Protocol / PublicDomain / Keepalive / etc. — a sparse file is functionally fine because Load re-applies defaults, but a user opening it for the first time would otherwise see a config that looks half-set.

func (*Config) FirstDifferentKnockResourceID

func (c *Config) FirstDifferentKnockResourceID(resourceID, knockResourceID string) (existingResourceID, existingKnockResourceID string, ok bool)

FirstDifferentKnockResourceID returns the first existing resource mapped to a non-empty NHP knock resource different from knockResourceID. Bootstrap uses it before SetKnockResourceID so a single connector cannot mix control resources. It sorts the current keys for deterministic conflict ordering; the cost is bounded by len(KnockResourceIDs) and is paid only before managed sessions start.

func (*Config) KnockResourceID

func (c *Config) KnockResourceID(resourceID string) string

KnockResourceID returns the logical NHP resource_id recorded for a qURL resource_id during device-owned resource hydration.

func (*Config) PrimaryResourceID

func (c *Config) PrimaryResourceID() string

PrimaryResourceID returns the first managed route's public resource identity. It is used for resource-indexed NHP metadata, never for routing.

func (*Config) SetKnockResourceID

func (c *Config) SetKnockResourceID(resourceID, knockResourceID string)

SetKnockResourceID records the logical NHP resource_id that should be knocked before dialing FRP for a qURL Connector resource. The key is the qURL resource_id (the public P-256 identity), not the slug or connector_routing_id.

type FileTransaction

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

FileTransaction pins the canonical config parent and owns its advisory lock. Every read and write is relative to that retained directory handle.

func AcquireFileTransaction

func AcquireFileTransaction(path string) (*FileTransaction, error)

AcquireFileTransaction locks path's canonical namespace. Close must be called for every successful acquisition. The default wait is bounded; callers with a command context should use AcquireFileTransactionContext.

func AcquireFileTransactionContext

func AcquireFileTransactionContext(ctx context.Context, path string) (*FileTransaction, error)

AcquireFileTransactionContext is AcquireFileTransaction with a caller-owned cancellation boundary. Transactions are deliberately non-reentrant: code holding one must call tx.Load/tx.Save, never Acquire or top-level Save.

func (*FileTransaction) Close

func (tx *FileTransaction) Close() error

Close releases the advisory lock after proving it still names the same pinned namespace entry. Any continuity failure is returned to the caller.

func (*FileTransaction) Exists

func (tx *FileTransaction) Exists() (bool, error)

Exists reports whether the transaction's config file currently exists.

func (*FileTransaction) Load

func (tx *FileTransaction) Load() (cfg *Config, retErr error)

Load reads and decodes the config from the pinned namespace.

func (*FileTransaction) Path

func (tx *FileTransaction) Path() string

Path is the canonical config path owned by this transaction.

func (*FileTransaction) ReplaceYAML

func (tx *FileTransaction) ReplaceYAML(data []byte, source string) error

ReplaceYAML validates and atomically stores one raw YAML document while preserving its comments and sparse shape. source is used only in validation diagnostics. Callers must already own this transaction.

func (*FileTransaction) Save

func (tx *FileTransaction) Save(cfg *Config) error

Save atomically writes cfg through the pinned namespace. Namespace and lock identity are revalidated before the first mutation and again before commit.

type ImmutableConfigSnapshot

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

ImmutableConfigSnapshot retains the config file and every directory handle from the filesystem root to its parent. It is the read-only counterpart to a FileTransaction for customer-managed bind mounts that cannot host a sibling lock file.

The snapshot never creates, chmods, syncs, renames, or removes filesystem objects. Callers must retain it until they have acquired the next lock in their lock order, then close it and join the result.

func (*ImmutableConfigSnapshot) Close

func (s *ImmutableConfigSnapshot) Close() error

Close revalidates continuity, closes the config descriptor, and releases all retained directory handles. Every failure is joined.

func (*ImmutableConfigSnapshot) Path

func (s *ImmutableConfigSnapshot) Path() string

Path is the canonical path pinned by the snapshot.

func (*ImmutableConfigSnapshot) RequireSiblingAbsent

func (s *ImmutableConfigSnapshot) RequireSiblingAbsent(name string) error

RequireSiblingAbsent proves name is absent in the same pinned namespace and retains that requirement for every later ValidateCurrent and Close. It revalidates the config on both sides so callers cannot accept a sibling transaction lock after either the config or its namespace changed.

func (*ImmutableConfigSnapshot) ValidateCurrent

func (s *ImmutableConfigSnapshot) ValidateCurrent() error

ValidateCurrent proves ancestor, parent, descriptor, and namespace-entry continuity for the already-read config.

type NHPConfig

type NHPConfig struct {
	Enabled   bool   `yaml:"enabled"`
	MachineID string `yaml:"machine_id,omitempty"`
}

NHPConfig holds Network Hiding Protocol settings.

type QURLConfig

type QURLConfig struct {
	APIURL string `yaml:"api_url,omitempty"`
	Token  string `yaml:"token,omitempty"`
}

QURLConfig holds qURL service integration settings.

type Route

type Route struct {
	// ID is the customer-facing route identifier. For Connector resources,
	// the registered-device qurl-go client sends this value verbatim as the
	// qURL resource slug when ResourceID is empty. The JSON tag intentionally omits
	// `omitempty` so list --json pollers always see a stable id key,
	// including the single-route env-fallback shape before resolution.
	ID            string            `yaml:"id,omitempty" json:"id"`
	Type          RouteType         `yaml:"type" json:"type"`
	LocalIP       string            `yaml:"local_ip,omitempty" json:"local_ip,omitempty"`
	LocalPort     int               `yaml:"local_port" json:"local_port"`
	RemotePort    int               `yaml:"remote_port,omitempty" json:"remote_port,omitempty"`
	Subdomain     string            `yaml:"subdomain,omitempty" json:"subdomain,omitempty"`
	CustomDomains []string          `yaml:"custom_domains,omitempty" json:"custom_domains,omitempty"`
	HostRewrite   string            `yaml:"host_rewrite,omitempty" json:"host_rewrite,omitempty"`
	Headers       map[string]string `yaml:"headers,omitempty" json:"headers,omitempty"`
	ResourceID    string            `yaml:"resource_id,omitempty" json:"resource_id,omitempty"`
	// ConnectorRoutingID is returned by the qURL control plane and used verbatim for
	// FRP SubDomain and load-balancer grouping. NHP placement comes from the
	// authenticated ACK instead. This value must never
	// be client-derived from or normalized against ResourceID; the control plane owns
	// the producer-side calculation.
	ConnectorRoutingID string `yaml:"connector_routing_id,omitempty" json:"connector_routing_id,omitempty"`
	TargetURL          string `yaml:"target_url,omitempty" json:"target_url,omitempty"`

	// LoadBalancerGroup wires FRP's HTTP/TCP loadBalancer.group +
	// groupKey so multiple sidecar replicas with the same slug register
	// under one routing key — FRPS load-balances requests across the
	// live replicas. Managed routes use ConnectorRoutingID as the one
	// authoritative value; this field may be omitted or must match it exactly.
	// Unmanaged/custom FRP routes retain the explicit field as before.
	LoadBalancerGroup string `yaml:"load_balancer_group,omitempty" json:"load_balancer_group,omitempty"`
}

Route describes a single proxy route.

Managed routes consume three separately carried producer values: ResourceID is the public qURL identity, ConnectorRoutingID is the FRP/HRW routing label, and Runtime.KnockResourceIDs holds the NHP admission target keyed by the public identity. Subdomain and LoadBalancerGroup are optional compatibility inputs only; when present on a managed route they must equal the opaque ConnectorRoutingID exactly.

type RouteType

type RouteType string

RouteType identifies the proxy protocol for a route.

const (
	// RouteTypeHTTP proxies HTTP traffic.
	RouteTypeHTTP RouteType = "http"
	// RouteTypeTCP proxies raw TCP traffic.
	RouteTypeTCP RouteType = "tcp"
)

func ParseTarget

func ParseTarget(raw string) (routeType RouteType, host string, port int, targetURL string, err error)

ParseTarget parses a user-supplied target URL (e.g. http://localhost:8080, tcp://10.0.0.5:5432) and returns the corresponding FRP route type, host, port, and the original URL. Empty hostname defaults to 127.0.0.1.

Schemes:

  • http -> RouteTypeHTTP, default port 80
  • https -> RouteTypeHTTP, default port 443
  • tcp -> RouteTypeTCP, explicit port required
  • ssh -> rejected with a hint to use tcp://host:port

type RuntimeConfig

type RuntimeConfig struct {
	// KnockResourceIDs maps qURL resource_id -> NHP knock resource_id.
	// Only SetKnockResourceID mutates it. Populated once during device-owned
	// resource hydration before the managed session starts; not safe for concurrent writes.
	KnockResourceIDs map[string]string
}

RuntimeConfig holds startup-derived process state that must never be serialized back into qurl-proxy.yaml. Customer-facing config stays on public LayerV endpoints; NHP resource metadata tells the agent what to knock, and the ACK supplies the FRP dial target.

type ServerConfig

type ServerConfig struct {
	Addr         string `yaml:"addr,omitempty"`
	Port         int    `yaml:"port,omitempty"`
	Token        string `yaml:"token,omitempty"`
	Protocol     string `yaml:"protocol,omitempty"`      // tcp, kcp, quic, websocket, wss
	PublicDomain string `yaml:"public_domain,omitempty"` // vhost domain for public URLs (e.g., qurl.site)

	// EgressLocalIP binds both the native NHP UDP socket and FRP's TCP/
	// websocket connection to one local source address. Multi-homed hosts must
	// set this explicitly so AC admission and the following Connector session
	// cannot leave through different interfaces. Only Protocol tcp, websocket,
	// wss, or empty may be combined with it — FRP never applies a local source
	// address to kcp/quic dials, so Validate rejects those combinations
	// instead of letting the session leave from the wrong IP and die at the
	// source-scoped boundary.
	EgressLocalIP string `yaml:"egress_local_ip,omitempty"`

	// Transport tuning for reconnection resilience.
	Keepalive     int   `yaml:"keepalive,omitempty"`       // TCP keepalive probe interval in seconds (default: 60)
	DialTimeout   int   `yaml:"dial_timeout,omitempty"`    // Server connection timeout in seconds (default: 10)
	LoginFailExit *bool `yaml:"login_fail_exit,omitempty"` // Exit on initial login failure (default: false)

	// ReplicaDiscriminator is the explicit per-process salt appended
	// to FRP proxy names (`<route.ID>-<discriminator>`) so multiple
	// replicas sharing a LoadBalancerGroup can register without
	// colliding on FRP's per-server-instance name uniqueness check
	// (server/control.go:484 emits `proxy [<name>] already exists`;
	// server/proxy/proxy.go:529 emits `proxy name [<name>] is
	// already in use`). The canonical resolution chain lives in
	// pkg/replica.Resolver — the YAML field here is the explicit
	// escape hatch for operators who want to set the salt from
	// outside the resolver (deterministic tests, bare-metal deploys
	// with a fixed-replica taxonomy). When set non-empty, run.go
	// bypasses the resolver chain. Empty (the headless default) →
	// resolver chain picks the salt at boot.
	//
	// The runtime path normally resolves a salt even for single-replica
	// deploys. If both this field AND the resolver return empty (today
	// only possible if a future CONFIG_REQUIRE_STABLE_DISCRIMINATOR
	// hard-fail mode is wired AND the operator declines all sources),
	// frpgen.go emits the raw route.ID as the proxy name for direct
	// config-generation compatibility.
	ReplicaDiscriminator string `yaml:"replica_discriminator,omitempty"`
}

ServerConfig holds connection details for the FRP server.

Jump to

Keyboard shortcuts

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