models

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Jun 13, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Index

Constants

View Source
const (
	OperatorRoleAdmin = "admin"
	OperatorRoleUser  = "user"
)

Operator roles. Operator.Role is a plain string; these are the only two values the server accepts.

View Source
const MaxAddressesPerHost = 16

MaxAddressesPerHost is the maximum number of overlay addresses a host can have. This is a soft limit to prevent cert bloat; Nebula v2 certs have no hard limit but practical deployments should not exceed this without good reason.

Variables

View Source
var ErrMobileRoleRestricted = errors.New("mobile hosts must have role=host (lighthouse/relay not supported)")

ErrMobileRoleRestricted is returned when a mobile host is assigned a role other than host (e.g., lighthouse or relay). Mobile Nebula clients cannot reliably listen on a public socket in the background, so these roles are not supported for mobile hosts.

View Source
var ErrMobileVariantRequired = errors.New("mobile hosts must have variant set to ios or android")

ErrMobileVariantRequired is returned when a mobile host is created without specifying a variant (ios or android). The variant field is required to distinguish mobile client types.

View Source
var ErrRoleRequiresListenPort = errors.New("listen_port is required when role is lighthouse or relay")

ErrRoleRequiresListenPort is the listen_port counterpart of ErrRoleRequiresPublicIP: peers compose static_host_map entries as "public_ip:listen_port", so a zero port produces the same silent failure.

View Source
var ErrRoleRequiresPublicIP = errors.New("public_ip is required when role is lighthouse or relay")

ErrRoleRequiresPublicIP is returned when a host with role=lighthouse or role=relay is created without a non-empty public_ip. Such a host would never be advertised to peers (see internal/api/enroll.go where static_host_map / lighthouse.hosts only include hosts whose PublicIP is set), so accept-and-silently-drop is replaced with reject-at-create.

Functions

func FriendlyAddrError

func FriendlyAddrError(field, value string) string

FriendlyAddrError returns a stable user-facing message for an IP-parse failure. The Go stdlib's netip.ParseAddr error text is intentionally dropped — strings like `ParseAddr("10.42.0.22.333"): IPv4 address too long` are diagnostic for Go authors but useless to operators typing into a form. Callers pass the form field name (or empty for unqualified messages) and the operator-supplied value so the message identifies both what is wrong and where.

func FriendlyPrefixError

func FriendlyPrefixError(field, value string) string

FriendlyPrefixError is the CIDR counterpart of FriendlyAddrError.

func HashEnrollmentToken

func HashEnrollmentToken(raw string) string

HashEnrollmentToken produces the at-rest representation of a raw enrollment token. Closes GHSA-ghmh-jhmj-wcmf: previously the raw UUID was stored verbatim in enrollment_tokens.token, allowing anyone with read access to the DB (backup, snapshot, future SQL-injection sink) to consume pending tokens before the legitimate agent.

Symmetric: same input → same hex → constant-time DB lookup. Mirrors the operator_api_keys hashing already done in internal/api/middleware.go.

func HashSessionToken added in v0.3.8

func HashSessionToken(raw string) string

HashSessionToken produces the at-rest representation of a raw operator session token. Closes GHSA-q4vm-pq3q-8wgq: previously the raw 32-byte hex token was stored verbatim in operator_sessions.token and sent in the session cookie, so anyone with read access to the DB (backup, snapshot, future SQL-injection sink) could hijack every active operator session.

Symmetric: same input → same hex → constant-time DB lookup. Mirrors the enrollment-token and operator_api_keys hashing already done elsewhere.

func HostDiff

func HostDiff(before, after *Host) ([]byte, bool, error)

HostDiff computes the difference between two hosts and returns a JSON-encoded map of changed fields. Returns (nil, false, nil) if no fields differ.

For basic fields (Name, NebulaIPs, Groups, Role, PublicIP, ListenPort), the diff key is the field name in snake_case.

For Advanced sub-fields (ListenHost, MTU, TunDevice, Punchy, UnsafeRoutes), the diff key uses dot-notation: "advanced.mtu", "advanced.punchy", etc.

The JSON format for each changed field is: {"field_name": {"before": <value>, "after": <value>}}

If before is nil, zero values are used. If before.Advanced is nil, zero values are used for sub-field comparisons.

func ValidKind

func ValidKind(k HostKind) bool

ValidKind reports whether k is a known host kind.

func ValidOperatorRole added in v0.4.0

func ValidOperatorRole(r string) bool

ValidOperatorRole reports whether r is a known operator role.

func ValidRole

func ValidRole(r HostRole) bool

ValidRole reports whether r is a known host role or empty (meaning "use default").

func ValidVariant

func ValidVariant(v HostVariant) bool

ValidVariant reports whether v is a known host variant (including empty).

func ValidateCIDR

func ValidateCIDR(field, value string) (netip.Prefix, error)

ValidateCIDR is a thin wrapper around netip.ParsePrefix that converts a failure into a user-facing FriendlyPrefixError.

func ValidateHostAddresses

func ValidateHostAddresses(addrs []string) error

ValidateHostAddresses validates a list of overlay addresses for a host. It checks that: - The list is non-empty - Each address is parseable - There are no duplicate addresses - The list does not exceed MaxAddressesPerHost

Note: This function does not check containment in parent network CIDRs. That validation is the responsibility of the caller (API or web handler) which has the parent network context and can provide better error messages.

func ValidateHostAdvanced

func ValidateHostAdvanced(adv *HostAdvanced) error

ValidateHostAdvanced rejects obviously broken advanced overrides before they reach the database. Empty / zero-value fields mean "inherit network default" and pass validation. All error messages use the user-facing friendly wrappers so the inline form (web) and the JSON API surface identical, stable strings.

func ValidateIPAddr

func ValidateIPAddr(field, value string) (netip.Addr, error)

ValidateIPAddr is a thin wrapper around netip.ParseAddr that converts a failure into a user-facing FriendlyAddrError. Returns the parsed address on success.

func ValidateMobileConstraints

func ValidateMobileConstraints(kind HostKind, variant HostVariant, role HostRole) error

ValidateMobileConstraints enforces role and variant constraints for mobile hosts. For kind=mobile, role must be host (or empty) and variant must be ios or android. For kind=agent, no constraints are applied (returns nil).

func ValidateNetworkCIDRs

func ValidateNetworkCIDRs(cidrs []string) error

ValidateNetworkCIDRs validates a list of CIDRs for a network. It checks that: - The list is non-empty - Each CIDR is parseable - There are no duplicate CIDRs - There are no overlapping CIDRs

func ValidateRoleReachability

func ValidateRoleReachability(role HostRole, publicIP string, listenPort int) error

ValidateRoleReachability rejects role/reachability combinations that would result in a silently-useless host. role=lighthouse and role=relay must both ship with a routable public_ip and a non-zero listen_port — otherwise peer config.yml renders an empty static_host_map and the host is never dialed (issue #94).

Types

type AuditEntry

type AuditEntry struct {
	ID        string    `json:"id"`
	Timestamp time.Time `json:"timestamp"`
	Actor     string    `json:"actor"`
	Action    string    `json:"action"`
	Resource  string    `json:"resource"`
	Details   string    `json:"details,omitempty"`
}

type CA

type CA struct {
	ID                   string    `json:"id"`
	Name                 string    `json:"name"`
	OwnerOperatorID      string    `json:"owner_operator_id"`
	CertPEM              string    `json:"cert_pem"`
	Fingerprint          string    `json:"fingerprint"`
	NotBefore            time.Time `json:"not_before"`
	NotAfter             time.Time `json:"not_after"`
	Status               CAStatus  `json:"status"`
	PredecessorID        *string   `json:"predecessor_id,omitempty"`
	EncryptedKeyDEK      []byte    `json:"-"`
	NonceDEK             []byte    `json:"-"`
	EncryptedKeyMaterial []byte    `json:"-"`
	NonceKey             []byte    `json:"-"`
	CreatedAt            time.Time `json:"created_at"`
	UpdatedAt            time.Time `json:"updated_at"`
}

CA is a per-operator certificate authority. Private key material lives only inside EncryptedKeyMaterial / NonceKey, wrapped under a per-CA DEK stored in EncryptedKeyDEK / NonceDEK (envelope encryption — see ADR 0002).

type CAStatus

type CAStatus string

CAStatus is the lifecycle status of a CA.

const (
	CAStatusActive  CAStatus = "active"
	CAStatusRetired CAStatus = "retired"
)

type CertificateInfo

type CertificateInfo struct {
	ID          string    `json:"id"`
	HostID      string    `json:"host_id"`
	Fingerprint string    `json:"fingerprint"`
	PEM         string    `json:"pem"`
	NotBefore   time.Time `json:"not_before"`
	NotAfter    time.Time `json:"not_after"`
	IsCurrent   bool      `json:"is_current"`
	CreatedAt   time.Time `json:"created_at"`
}

type EnrollmentToken

type EnrollmentToken struct {
	ID        string     `json:"id"`
	HostID    string     `json:"host_id"`
	TokenHash string     `json:"-"` // SHA-256 hex; raw value never persisted (GHSA-ghmh-jhmj-wcmf)
	Used      bool       `json:"used"`
	ExpiresAt time.Time  `json:"expires_at"`
	UsedAt    *time.Time `json:"used_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

type Host

type Host struct {
	ID                  string        `json:"id"`
	NetworkID           string        `json:"network_id"`
	CAID                string        `json:"ca_id,omitempty"`
	Name                string        `json:"name"`
	NebulaIPs           []string      `json:"nebula_ips"`
	Groups              []string      `json:"groups"`
	Role                HostRole      `json:"role"`
	IsLighthouse        bool          `json:"is_lighthouse"`
	IsRelay             bool          `json:"is_relay"`
	PublicIP            string        `json:"public_ip,omitempty"`
	ListenPort          int           `json:"listen_port,omitempty"`
	Status              HostStatus    `json:"status"`
	CertFingerprint     string        `json:"cert_fingerprint,omitempty"`
	PrevCertFingerprint string        `json:"prev_cert_fingerprint,omitempty"`
	CertExpiresAt       *time.Time    `json:"cert_expires_at,omitempty"`
	CertRotatedAt       *time.Time    `json:"cert_rotated_at,omitempty"`
	PendingRekey        bool          `json:"pending_rekey,omitempty"`
	SigningPubPEM       string        `json:"signing_pub_pem,omitempty"`
	LastSeenAt          *time.Time    `json:"last_seen_at,omitempty"`
	Advanced            *HostAdvanced `json:"advanced,omitempty"`
	Kind                HostKind      `json:"kind"`
	Variant             HostVariant   `json:"variant,omitempty"`
	CreatedAt           time.Time     `json:"created_at"`
	UpdatedAt           time.Time     `json:"updated_at"`
}

type HostAdvanced

type HostAdvanced struct {
	Punchy       *bool         `json:"punchy,omitempty" yaml:"punchy,omitempty"`
	ListenHost   string        `json:"listen_host,omitempty" yaml:"listen_host,omitempty"`
	MTU          int           `json:"mtu,omitempty" yaml:"mtu,omitempty"`
	TunDevice    string        `json:"tun_device,omitempty" yaml:"tun_device,omitempty"`
	UnsafeRoutes []UnsafeRoute `json:"unsafe_routes,omitempty" yaml:"unsafe_routes,omitempty"`
}

HostAdvanced groups optional per-host overrides for the rendered Nebula config. All fields are optional. A field set to its zero value means "inherit network default"; a field set to a non-zero value overrides.

Punchy is a tri-state pointer so an operator can explicitly disable hole-punching for a host (false) without it being indistinguishable from "not set".

type HostKind

type HostKind string
const (
	HostKindAgent  HostKind = "agent"
	HostKindMobile HostKind = "mobile"
)

type HostRole

type HostRole string
const (
	HostRoleHost       HostRole = "host"
	HostRoleLighthouse HostRole = "lighthouse"
	HostRoleRelay      HostRole = "relay"
)

type HostStatus

type HostStatus string
const (
	HostStatusPending  HostStatus = "pending"
	HostStatusEnrolled HostStatus = "enrolled"
	HostStatusBlocked  HostStatus = "blocked"
)

type HostVariant

type HostVariant string
const (
	HostVariantNone    HostVariant = ""
	HostVariantIOS     HostVariant = "ios"
	HostVariantAndroid HostVariant = "android"
)

type Network

type Network struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	CIDRs     []string  `json:"cidrs"`
	CAID      string    `json:"ca_id,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

type Operator

type Operator struct {
	ID           string               `json:"id"`
	Username     string               `json:"username"`
	DisplayName  string               `json:"display_name"`
	PasswordHash string               `json:"-"`
	AuthProvider OperatorAuthProvider `json:"auth_provider"`
	Status       OperatorStatus       `json:"status"`
	Role         string               `json:"role"`
	TOTPSecret   string               `json:"-"`
	TOTPEnabled  bool                 `json:"totp_enabled"`
	OIDCIssuer   string               `json:"oidc_issuer,omitempty"`
	OIDCSubject  string               `json:"oidc_subject,omitempty"`
	CreatedAt    time.Time            `json:"created_at"`
	UpdatedAt    time.Time            `json:"updated_at"`
	LastLoginAt  *time.Time           `json:"last_login_at,omitempty"`
}

Operator is an administrative user of the management server.

type OperatorAPIKey

type OperatorAPIKey struct {
	ID         string     `json:"id"`
	OperatorID string     `json:"operator_id"`
	Name       string     `json:"name"`
	KeyHash    string     `json:"-"`
	CreatedAt  time.Time  `json:"created_at"`
	LastUsedAt *time.Time `json:"last_used_at,omitempty"`
	RevokedAt  *time.Time `json:"revoked_at,omitempty"`
}

OperatorAPIKey is a per-operator API key. Only the hash is stored.

type OperatorAuthProvider

type OperatorAuthProvider string

OperatorAuthProvider identifies the authentication backend for an operator.

const (
	OperatorAuthLocal OperatorAuthProvider = "local"
	OperatorAuthOIDC  OperatorAuthProvider = "oidc"
)

type OperatorSession

type OperatorSession struct {
	// Token is the raw session token carried in the operator's cookie. It is
	// transient: the store persists only HashSessionToken(Token), never the
	// raw value at rest (GHSA-q4vm-pq3q-8wgq).
	Token      string
	OperatorID string
	State      SessionState
	ExpiresAt  time.Time
	CreatedAt  time.Time
}

OperatorSession represents a UI session. A session in `pending_totp` state is awaiting a second-factor verification and is not yet authenticated.

type OperatorStatus

type OperatorStatus string

OperatorStatus represents the active/disabled state of an operator.

const (
	OperatorStatusActive   OperatorStatus = "active"
	OperatorStatusDisabled OperatorStatus = "disabled"
)

type SessionState

type SessionState string

SessionState is the lifecycle phase of an operator session.

const (
	SessionStateAuthenticated SessionState = "authenticated"
	SessionStatePendingTOTP   SessionState = "pending_totp"
)

type UnsafeRoute

type UnsafeRoute struct {
	Route string `json:"route" yaml:"route"` // CIDR
	Via   string `json:"via" yaml:"via"`     // Nebula IP of the gateway host
}

UnsafeRoute is a single "unsafe route" entry: traffic for `Route` is sent through the host with Nebula IP `Via`. See Nebula's tun.unsafe_routes.

type WebhookSubscription added in v0.6.0

type WebhookSubscription struct {
	ID              string   `json:"id"`
	OwnerOperatorID string   `json:"owner_operator_id"`
	URL             string   `json:"url"`
	Events          []string `json:"events"` // empty = all events
	Active          bool     `json:"active"`
	AllowPrivate    bool     `json:"allow_private"`

	// Envelope-encrypted HMAC secret. All nil => unsigned deliveries.
	EncryptedSecretDEK []byte `json:"-"`
	NonceDEK           []byte `json:"-"`
	EncryptedSecret    []byte `json:"-"`
	NonceSecret        []byte `json:"-"`

	// HasSecret is a computed, response-only flag (the secret itself is never
	// returned). It is not persisted as a column.
	HasSecret bool `json:"has_secret"`

	// Per-subscription delivery observability.
	LastDeliveryAt      *time.Time `json:"last_delivery_at,omitempty"`
	LastStatus          string     `json:"last_status,omitempty"`
	LastError           string     `json:"last_error,omitempty"`
	ConsecutiveFailures int        `json:"consecutive_failures"`

	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

WebhookSubscription is an operator-owned outbound webhook target (#256 phase 2). The HMAC secret is stored envelope-encrypted (a per-row DEK wrapped under the master key, the secret sealed under the DEK), so the encrypted fields never serialize to JSON; API responses expose only HasSecret.

Jump to

Keyboard shortcuts

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