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
- Variables
- func AdminBindLooksRoutable(cfg *Config) bool
- func AdminURL(addr string, port int) string
- func CanonicalPath(path string) (string, error)
- func Discover(configFlag string) (string, error)
- func ExecutableDir() (string, error)
- func FRPProxyName(routeID, replicaDiscriminator string) string
- func GenerateFRPClientConfig(cfg *Config, machineID string) (*v1.ClientCommonConfig, []v1.ProxyConfigurer, []v1.VisitorConfigurer, error)
- func LocalAdminURL(addr string, port int) string
- func OpenImmutableConfigSnapshot(path string) (_ *Config, snapshot *ImmutableConfigSnapshot, retErr error)
- func Save(cfg *Config, path string) error
- func Validate(cfg *Config) error
- func ValidateConnectorRoutingID(s string) error
- func ValidateManagedRouteIdentities(routes []Route) error
- func ValidateSlug(s string) error
- func WithFileTransaction(path string, fn func(*FileTransaction) error) (retErr error)
- func WithFileTransactionContext(ctx context.Context, path string, fn func(*FileTransaction) error) (retErr error)
- type AdminConfig
- type AuditConfig
- type Config
- func (c *Config) FirstDifferentKnockResourceID(resourceID, knockResourceID string) (existingResourceID, existingKnockResourceID string, ok bool)
- func (c *Config) KnockResourceID(resourceID string) string
- func (c *Config) PrimaryResourceID() string
- func (c *Config) SetKnockResourceID(resourceID, knockResourceID string)
- type FileTransaction
- func (tx *FileTransaction) Close() error
- func (tx *FileTransaction) Exists() (bool, error)
- func (tx *FileTransaction) Load() (cfg *Config, retErr error)
- func (tx *FileTransaction) Path() string
- func (tx *FileTransaction) ReplaceYAML(data []byte, source string) error
- func (tx *FileTransaction) Save(cfg *Config) error
- type ImmutableConfigSnapshot
- type NHPConfig
- type QURLConfig
- type Route
- type RouteType
- type RuntimeConfig
- type ServerConfig
Constants ¶
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.
const DefaultAdminPort = 7400
DefaultAdminPort is the TCP port FRP's admin API binds when enabled.
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.
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.
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.
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.
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.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
Discover locates the configuration file to use. It checks, in order:
- The explicit path from configFlag (error if not found).
- ./qurl-proxy.yaml in the current working directory.
- <binary_dir>/etc/qurl-proxy.yaml alongside the running binary.
- ~/.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 ¶
ExecutableDir returns the directory containing the running binary, resolving any symlinks.
func FRPProxyName ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
KnockResourceID returns the logical NHP resource_id recorded for a qURL resource_id during device-owned resource hydration.
func (*Config) PrimaryResourceID ¶
PrimaryResourceID returns the first managed route's public resource identity. It is used for resource-indexed NHP metadata, never for routing.
func (*Config) SetKnockResourceID ¶
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.
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.