// Package agentd implements the Pangolin node agent: a lightweight daemon that // runs on every acceleration node. It self-enrolls over mTLS, dials the control // plane (the agent is always the dialer), keeps a long-lived gRPC connection with // exponential-backoff reconnect, reports heartbeat/usage, and applies the streamed // command feed by managing the local sing-box user table. // // No-state invariant (doc/04 §2, doc/06 §3): the agent persists ONLY the // credential table (dp_uuid + expires_at) to disk. It keeps zero user identities, // zero destination/DNS data and writes no access logs. A seized node leaks only // opaque dp_uuids, never accounts. package agentd import ( "path/filepath" "time" ) // Default tuning values. The intervals match doc/06 (30s heartbeat) and the // 500ms render debounce that batches bursts of credential changes into a single // sing-box restart (sing-box has no hot-reload; a restart is a ~1-2s blip). const ( DefaultHeartbeatInterval = 30 * time.Second DefaultUsageInterval = 60 * time.Second DefaultTTLScanInterval = 15 * time.Second DefaultDebounceWindow = 500 * time.Millisecond DefaultDialTimeout = 10 * time.Second DefaultBackoffMin = 1 * time.Second DefaultBackoffMax = 60 * time.Second DefaultStateDir = "/etc/pangolin-agent" DefaultSingboxCfg = "/etc/sing-box/config.json" // DefaultFlow is the REALITY VLESS flow (doc/02 §3.1). // Kept here as an alias; canonical definition is in internal/dpcred. DefaultFlow = "xtls-rprx-vision" ) // Config holds everything the agent needs. Most fields come from cloud-init // (control-plane address + bootstrap token) or have safe defaults. type Config struct { // ControlPlaneAddr is the control plane gRPC endpoint (host:port). ControlPlaneAddr string // ServerName overrides the TLS SNI used when dialing (defaults to the host // part of ControlPlaneAddr). The control plane cert must match it. ServerName string // BootstrapToken is the one-time cloud-init token consumed during Enroll. // Ignored once the node already holds a certificate. BootstrapToken string // StateDir holds node.key/node.crt/ca.crt and state.json. StateDir string // SingboxConfigPath is where the rendered sing-box config is written. SingboxConfigPath string // DeriveKey keys the Hy2 password derivation (see DeriveHy2Password). DeriveKey string AgentVersion string HeartbeatInterval time.Duration UsageInterval time.Duration TTLScanInterval time.Duration DebounceWindow time.Duration DialTimeout time.Duration BackoffMin time.Duration BackoffMax time.Duration // Insecure dials without mTLS. ONLY for local dev / integration tests. Insecure bool } // withDefaults returns a copy of c with zero-valued tunables filled in. func (c Config) withDefaults() Config { if c.StateDir == "" { c.StateDir = DefaultStateDir } if c.SingboxConfigPath == "" { c.SingboxConfigPath = DefaultSingboxCfg } if c.HeartbeatInterval == 0 { c.HeartbeatInterval = DefaultHeartbeatInterval } if c.UsageInterval == 0 { c.UsageInterval = DefaultUsageInterval } if c.TTLScanInterval == 0 { c.TTLScanInterval = DefaultTTLScanInterval } if c.DebounceWindow == 0 { c.DebounceWindow = DefaultDebounceWindow } if c.DialTimeout == 0 { c.DialTimeout = DefaultDialTimeout } if c.BackoffMin == 0 { c.BackoffMin = DefaultBackoffMin } if c.BackoffMax == 0 { c.BackoffMax = DefaultBackoffMax } if c.AgentVersion == "" { c.AgentVersion = "dev" } return c } // Paths to the on-disk PKI + state material. func (c Config) KeyPath() string { return filepath.Join(c.StateDir, "node.key") } func (c Config) CertPath() string { return filepath.Join(c.StateDir, "node.crt") } func (c Config) CAPath() string { return filepath.Join(c.StateDir, "ca.crt") } func (c Config) StatePath() string { return filepath.Join(c.StateDir, "state.json") }