mirror of
https://github.com/slackhq/nebula.git
synced 2026-07-01 19:10:29 +02:00
Tighten host_query comments to match project style
https://claude.ai/code/session_01Nibp24Pgk2JMue8VyWHq7o
This commit is contained in:
+53
-82
@@ -22,20 +22,17 @@ import (
|
|||||||
"github.com/slackhq/nebula/config"
|
"github.com/slackhq/nebula/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hostQueryServer owns the local host query API: a small HTTP+JSON listener
|
// hostQueryServer is a small http+json listener on a unix socket or tcp address that lets other
|
||||||
// on a unix socket or tcp address that lets other programs on this machine
|
// programs on this machine resolve a vpn address to its certificate identity (name, groups, networks)
|
||||||
// resolve a vpn address to its certificate identity (name, groups, networks)
|
// for making authorization decisions. Lifecycle works like statsServer: the constructor wires the
|
||||||
// for making authorization decisions. It mirrors the lifecycle shape of
|
// reload callback, reload records config, Start runs the runtime, Stop tears it down
|
||||||
// statsServer: constructor wires the reload callback, reload records config,
|
|
||||||
// Start builds and runs the runtime, Stop tears it down.
|
|
||||||
type hostQueryServer struct {
|
type hostQueryServer struct {
|
||||||
l *slog.Logger
|
l *slog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
hostMap *HostMap
|
hostMap *HostMap
|
||||||
pki *PKI
|
pki *PKI
|
||||||
|
|
||||||
// enabled mirrors `host_query.enabled`. Start consults it so callers
|
// enabled mirrors `host_query.enabled` so callers of Start don't need to know the gating rules
|
||||||
// don't need to know the gating rules.
|
|
||||||
enabled atomic.Bool
|
enabled atomic.Bool
|
||||||
|
|
||||||
runMu sync.Mutex
|
runMu sync.Mutex
|
||||||
@@ -43,35 +40,28 @@ type hostQueryServer struct {
|
|||||||
run *hostQueryRuntime // non-nil while a runtime is live
|
run *hostQueryRuntime // non-nil while a runtime is live
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostQueryRuntime is the live state owned by a single Start invocation.
|
// hostQueryRuntime is the live state owned by a single Start invocation. Stop and Start's exit path
|
||||||
// Start stashes a pointer under runMu; Stop and Start's own exit path use
|
// use pointer equality to tell "my runtime" apart from one that replaced it after a reload
|
||||||
// pointer equality to tell "my runtime" apart from one that replaced it
|
|
||||||
// after a reload.
|
|
||||||
type hostQueryRuntime struct {
|
type hostQueryRuntime struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostQueryConfig is the snapshot of host_query config that drives the
|
// hostQueryConfig is a snapshot of the host_query config section, comparable with == so reload can
|
||||||
// runtime. It is comparable with == so reload can detect "no change" cheaply.
|
// detect "no change" cheaply
|
||||||
type hostQueryConfig struct {
|
type hostQueryConfig struct {
|
||||||
enabled bool
|
enabled bool
|
||||||
listen string // raw config value, for error messages
|
listen string // raw config value, for error messages
|
||||||
network string // "unix" or "tcp"
|
network string // "unix" or "tcp"
|
||||||
addr string // socket path or host:port
|
addr string // socket path or host:port
|
||||||
// socketMode is the file mode applied to the unix socket after bind.
|
// file mode applied to the unix socket after bind
|
||||||
socketMode fs.FileMode
|
socketMode fs.FileMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// newHostQueryServerFromConfig builds a hostQueryServer, applies the initial
|
// newHostQueryServerFromConfig builds a hostQueryServer and applies the initial config. The reload
|
||||||
// config, and registers a reload callback. The reload callback is registered
|
// callback is registered first so a SIGHUP can later enable, fix, or disable the listener even if
|
||||||
// before the initial config is applied so a SIGHUP can later enable, fix, or
|
// the initial config was bad. Nothing binds until Start, so config tests are side effect free.
|
||||||
// disable the listener even if the initial application failed.
|
// The returned pointer is always non-nil, even on error
|
||||||
//
|
|
||||||
// Construction never binds the listener; that happens in Start, so config
|
|
||||||
// tests are side effect free. Start is safe to call unconditionally: it
|
|
||||||
// no-ops when the host query API is disabled. The returned pointer is always
|
|
||||||
// non-nil, even on error.
|
|
||||||
func newHostQueryServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI, hostMap *HostMap, c *config.C) (*hostQueryServer, error) {
|
func newHostQueryServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI, hostMap *HostMap, c *config.C) (*hostQueryServer, error) {
|
||||||
h := &hostQueryServer{
|
h := &hostQueryServer{
|
||||||
l: l,
|
l: l,
|
||||||
@@ -92,14 +82,9 @@ func newHostQueryServerFromConfig(ctx context.Context, l *slog.Logger, pki *PKI,
|
|||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reload records the latest config. On the initial call it only records it;
|
// reload records the latest config. The initial call only records it, Control.Start launches the
|
||||||
// Control.Start is what launches the first runtime via hostQueryStart. On
|
// first runtime via hostQueryStart. Later calls reconcile the running listener with the new config:
|
||||||
// later calls it reconciles the running runtime with the new config:
|
// enable, disable, or restart when the listen config changed
|
||||||
//
|
|
||||||
// - newly enabled -> spawn Start
|
|
||||||
// - newly disabled -> Stop the runtime
|
|
||||||
// - config changed (still enabled) -> Stop the old, Start the new
|
|
||||||
// - no change -> no-op
|
|
||||||
func (h *hostQueryServer) reload(c *config.C, initial bool) error {
|
func (h *hostQueryServer) reload(c *config.C, initial bool) error {
|
||||||
newCfg, err := loadHostQueryConfig(c)
|
newCfg, err := loadHostQueryConfig(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,9 +112,8 @@ func (h *hostQueryServer) reload(c *config.C, initial bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start binds the listener from the latest config and serves until Stop is
|
// Start binds the listener from the latest config and serves until Stop is called or ctx fires.
|
||||||
// called or ctx fires. Safe to call when the host query API is disabled or
|
// Safe to call when disabled or already running (both no-op)
|
||||||
// already running (both no-op).
|
|
||||||
func (h *hostQueryServer) Start() {
|
func (h *hostQueryServer) Start() {
|
||||||
if !h.enabled.Load() {
|
if !h.enabled.Load() {
|
||||||
return
|
return
|
||||||
@@ -143,8 +127,7 @@ func (h *hostQueryServer) Start() {
|
|||||||
cfg := *h.runCfg
|
cfg := *h.runCfg
|
||||||
ln, err := h.listen(cfg)
|
ln, err := h.listen(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Drop the cached config so a SIGHUP with the same config re-triggers
|
// drop the cached config so a SIGHUP with the same config retries the bind
|
||||||
// Start once the user fixes the underlying problem.
|
|
||||||
h.runCfg = nil
|
h.runCfg = nil
|
||||||
h.runMu.Unlock()
|
h.runMu.Unlock()
|
||||||
h.l.Error("Failed to start host query listener", "listen", cfg.listen, "error", err)
|
h.l.Error("Failed to start host query listener", "listen", cfg.listen, "error", err)
|
||||||
@@ -162,19 +145,17 @@ func (h *hostQueryServer) Start() {
|
|||||||
h.l.Info("Starting host query listener", "network", cfg.network, "addr", ln.Addr())
|
h.l.Info("Starting host query listener", "network", cfg.network, "addr", ln.Addr())
|
||||||
cleanExit := h.serve(srv, ln)
|
cleanExit := h.serve(srv, ln)
|
||||||
|
|
||||||
// A Stop that raced our bind shut the server down before Serve could
|
// A Stop that raced our bind shut the server down before Serve could adopt the listener;
|
||||||
// adopt the listener; closing it again is harmless and guarantees a unix
|
// closing it again is harmless and guarantees a unix socket file gets unlinked
|
||||||
// socket file gets unlinked.
|
|
||||||
_ = ln.Close()
|
_ = ln.Close()
|
||||||
|
|
||||||
// Clear our runtime only if nothing has replaced it. Stop races through
|
// Clear our runtime only if nothing has replaced it. Stop races through here too but leaves
|
||||||
// here too but leaves h.run == nil, so the pointer check skips.
|
// h.run == nil, so the pointer check skips
|
||||||
h.runMu.Lock()
|
h.runMu.Lock()
|
||||||
if h.run == rt {
|
if h.run == rt {
|
||||||
h.run = nil
|
h.run = nil
|
||||||
// A listener that exited with an error leaves runCfg cached as if it
|
// an error exit leaves runCfg cached as if it were applied, drop it so a SIGHUP with the
|
||||||
// were applied. Drop it so a SIGHUP with the same config re-triggers
|
// same config re-triggers Start once the user fixes the underlying problem
|
||||||
// Start once the user fixes the underlying problem.
|
|
||||||
if !cleanExit {
|
if !cleanExit {
|
||||||
h.runCfg = nil
|
h.runCfg = nil
|
||||||
}
|
}
|
||||||
@@ -182,13 +163,11 @@ func (h *hostQueryServer) Start() {
|
|||||||
h.runMu.Unlock()
|
h.runMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// serve runs srv.Serve and ensures ctx cancellation unblocks it. Returns true
|
// serve runs srv.Serve and ensures ctx cancellation unblocks it. Returns true if the listener
|
||||||
// if the listener exited cleanly (Stop, ctx cancellation, or any other
|
// exited cleanly (Stop, ctx cancellation), false on an unexpected error
|
||||||
// http.ErrServerClosed path), false on an unexpected error.
|
|
||||||
func (h *hostQueryServer) serve(srv *http.Server, ln net.Listener) bool {
|
func (h *hostQueryServer) serve(srv *http.Server, ln net.Listener) bool {
|
||||||
// Per-invocation watcher: ctx cancellation triggers a server shutdown
|
// ctx cancellation triggers a server shutdown which in turn unblocks Serve, closing `done` on
|
||||||
// which in turn unblocks Serve. Closing `done` on exit keeps the watcher
|
// exit keeps the watcher from outliving this call
|
||||||
// from outliving this call.
|
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
select {
|
select {
|
||||||
@@ -211,7 +190,7 @@ func (h *hostQueryServer) serve(srv *http.Server, ln net.Listener) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop tears down the active runtime, if any. Idempotent.
|
// Stop tears down the active runtime, if any. Idempotent
|
||||||
func (h *hostQueryServer) Stop() {
|
func (h *hostQueryServer) Stop() {
|
||||||
h.runMu.Lock()
|
h.runMu.Lock()
|
||||||
rt := h.run
|
rt := h.run
|
||||||
@@ -227,9 +206,8 @@ func (h *hostQueryServer) Stop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// listen binds the configured address. For unix sockets it also clears a
|
// listen binds the configured address. For unix sockets it also clears a stale socket file left by
|
||||||
// stale socket file left by an unclean exit and applies the configured file
|
// an unclean exit and applies the configured file mode
|
||||||
// mode.
|
|
||||||
func (h *hostQueryServer) listen(cfg hostQueryConfig) (net.Listener, error) {
|
func (h *hostQueryServer) listen(cfg hostQueryConfig) (net.Listener, error) {
|
||||||
if cfg.network == "unix" {
|
if cfg.network == "unix" {
|
||||||
return h.listenUnix(cfg)
|
return h.listenUnix(cfg)
|
||||||
@@ -249,9 +227,8 @@ func (h *hostQueryServer) listenUnix(cfg hostQueryConfig) (net.Listener, error)
|
|||||||
if fi.Mode()&os.ModeSocket == 0 {
|
if fi.Mode()&os.ModeSocket == 0 {
|
||||||
return nil, fmt.Errorf("host_query.listen path %s exists and is not a socket, refusing to replace it", cfg.addr)
|
return nil, fmt.Errorf("host_query.listen path %s exists and is not a socket, refusing to replace it", cfg.addr)
|
||||||
}
|
}
|
||||||
// A normal shutdown unlinks the socket (unlink-on-close), so a file
|
// a normal shutdown unlinks the socket, so a file here means a previous process exited
|
||||||
// here means a previous process exited uncleanly. Remove it so the
|
// uncleanly, remove it so the bind below can succeed
|
||||||
// bind below can succeed.
|
|
||||||
if err = os.Remove(cfg.addr); err != nil {
|
if err = os.Remove(cfg.addr); err != nil {
|
||||||
return nil, fmt.Errorf("failed to remove stale socket %s: %w", cfg.addr, err)
|
return nil, fmt.Errorf("failed to remove stale socket %s: %w", cfg.addr, err)
|
||||||
}
|
}
|
||||||
@@ -261,9 +238,8 @@ func (h *hostQueryServer) listenUnix(cfg hostQueryConfig) (net.Listener, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// The socket is briefly live with umask-derived permissions before this
|
// The socket is briefly live with umask-derived permissions before this chmod lands, tolerated
|
||||||
// chmod lands; tolerated because connections accepted in that window
|
// because connections accepted in that window still only reach this read-only API
|
||||||
// still only reach this read-only API.
|
|
||||||
if err = os.Chmod(cfg.addr, cfg.socketMode); err != nil {
|
if err = os.Chmod(cfg.addr, cfg.socketMode); err != nil {
|
||||||
_ = ln.Close()
|
_ = ln.Close()
|
||||||
return nil, fmt.Errorf("failed to set mode on socket %s: %w", cfg.addr, err)
|
return nil, fmt.Errorf("failed to set mode on socket %s: %w", cfg.addr, err)
|
||||||
@@ -278,10 +254,9 @@ func (h *hostQueryServer) certState() *CertState {
|
|||||||
return h.pki.getCertState()
|
return h.pki.getCertState()
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleHost serves GET /v1/host?addr=<vpn addr>, answering with the identity
|
// handleHost serves GET /v1/host?addr=<vpn addr>, answering with the identity of the host that
|
||||||
// of the host that owns the address: a peer with an active tunnel, or this
|
// owns the address: a peer with an active tunnel, or this node itself. addr may include a port,
|
||||||
// node itself. addr may include a port, which is ignored, so clients can pass
|
// which is ignored, so clients can pass a connection's remote address through without parsing it
|
||||||
// a connection's remote address through without parsing it.
|
|
||||||
func (h *hostQueryServer) handleHost(w http.ResponseWriter, r *http.Request) {
|
func (h *hostQueryServer) handleHost(w http.ResponseWriter, r *http.Request) {
|
||||||
q := r.URL.Query().Get("addr")
|
q := r.URL.Query().Get("addr")
|
||||||
if q == "" {
|
if q == "" {
|
||||||
@@ -302,7 +277,7 @@ func (h *hostQueryServer) handleHost(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.writeHostIdentity(w, crt)
|
h.writeHostIdentity(w, crt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleSelf serves GET /v1/self, answering with this node's own identity.
|
// handleSelf serves GET /v1/self, answering with this node's own identity
|
||||||
func (h *hostQueryServer) handleSelf(w http.ResponseWriter, r *http.Request) {
|
func (h *hostQueryServer) handleSelf(w http.ResponseWriter, r *http.Request) {
|
||||||
var crt cert.Certificate
|
var crt cert.Certificate
|
||||||
if cs := h.certState(); cs != nil {
|
if cs := h.certState(); cs != nil {
|
||||||
@@ -327,10 +302,9 @@ func (h *hostQueryServer) writeHostIdentity(w http.ResponseWriter, crt cert.Cert
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// findCertificateForVpnAddr answers "who owns this vpn address": ourselves
|
// findCertificateForVpnAddr answers "who owns this vpn address": ourselves (from local cert state,
|
||||||
// (from local cert state, since the hostmap never carries an entry for this
|
// the hostmap never carries an entry for this node) or a peer with an active tunnel. Returns nil
|
||||||
// node) or a peer with an active tunnel. Returns nil when the address is
|
// when the address is unknown or the tunnel is mid-teardown
|
||||||
// unknown or the tunnel is mid-teardown.
|
|
||||||
func findCertificateForVpnAddr(cs *CertState, hostMap *HostMap, ip netip.Addr) cert.Certificate {
|
func findCertificateForVpnAddr(cs *CertState, hostMap *HostMap, ip netip.Addr) cert.Certificate {
|
||||||
if cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
|
if cs != nil && cs.myVpnAddrsTable != nil && cs.myVpnAddrsTable.Contains(ip) {
|
||||||
return cs.getCertificate(cs.initiatingVersion)
|
return cs.getCertificate(cs.initiatingVersion)
|
||||||
@@ -347,8 +321,8 @@ func findCertificateForVpnAddr(cs *CertState, hostMap *HostMap, ip netip.Addr) c
|
|||||||
return cc.Certificate
|
return cc.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
// hostIdentity is the JSON document served for both /v1/host and /v1/self.
|
// hostIdentity is the json document served for both /v1/host and /v1/self, every field is derived
|
||||||
// Every field is derived from the authenticated certificate alone.
|
// from the authenticated certificate alone
|
||||||
type hostIdentity struct {
|
type hostIdentity struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
VpnAddrs []netip.Addr `json:"vpnAddrs"`
|
VpnAddrs []netip.Addr `json:"vpnAddrs"`
|
||||||
@@ -368,8 +342,7 @@ func newHostIdentity(crt cert.Certificate) (hostIdentity, error) {
|
|||||||
return hostIdentity{}, err
|
return hostIdentity{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slices are always allocated so they marshal as [] rather than null;
|
// slices are always allocated so they marshal as [] rather than null
|
||||||
// consumers iterate groups without a presence check.
|
|
||||||
networks := crt.Networks()
|
networks := crt.Networks()
|
||||||
id := hostIdentity{
|
id := hostIdentity{
|
||||||
Name: crt.Name(),
|
Name: crt.Name(),
|
||||||
@@ -395,10 +368,9 @@ func writeJSONError(w http.ResponseWriter, status int, msg string) {
|
|||||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseQueryAddrParam parses the addr query parameter, accepting a bare
|
// parseQueryAddrParam parses the addr query parameter, accepting a bare address or an address with
|
||||||
// address or an address with a port (`192.168.100.7:54321`, `[fd00::1]:443`)
|
// a port (`192.168.100.7:54321`, `[fd00::1]:443`) so callers can pass a connection's RemoteAddr
|
||||||
// so callers can pass a connection's RemoteAddr straight through. The result
|
// straight through. The result is unmapped, 4in6 addresses (::ffff:a.b.c.d) become ipv4
|
||||||
// is unmapped: 4in6 addresses (::ffff:a.b.c.d) are normalized to ipv4.
|
|
||||||
func parseQueryAddrParam(s string) (netip.Addr, error) {
|
func parseQueryAddrParam(s string) (netip.Addr, error) {
|
||||||
if ip, err := netip.ParseAddr(s); err == nil {
|
if ip, err := netip.ParseAddr(s); err == nil {
|
||||||
return ip.Unmap(), nil
|
return ip.Unmap(), nil
|
||||||
@@ -430,7 +402,7 @@ func loadHostQueryConfig(c *config.C) (hostQueryConfig, error) {
|
|||||||
cfg.addr = addr
|
cfg.addr = addr
|
||||||
|
|
||||||
if network == "unix" {
|
if network == "unix" {
|
||||||
// Read as a string so YAML can't reinterpret the octal literal.
|
// read as a string so yaml can't reinterpret the octal literal
|
||||||
modeStr := c.GetString("host_query.socket_mode", "0600")
|
modeStr := c.GetString("host_query.socket_mode", "0600")
|
||||||
mode, err := strconv.ParseUint(modeStr, 8, 32)
|
mode, err := strconv.ParseUint(modeStr, 8, 32)
|
||||||
if err != nil || fs.FileMode(mode)&^fs.ModePerm != 0 {
|
if err != nil || fs.FileMode(mode)&^fs.ModePerm != 0 {
|
||||||
@@ -441,9 +413,8 @@ func loadHostQueryConfig(c *config.C) (hostQueryConfig, error) {
|
|||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseHostQueryListen splits the host_query.listen config value into a
|
// parseHostQueryListen splits the host_query.listen config value into a network and address for
|
||||||
// network and address for net.Listen: `unix:///abs/path.sock` selects a unix
|
// net.Listen: `unix:///abs/path.sock` selects a unix socket, anything else must be a tcp host:port
|
||||||
// socket, anything else must be a tcp host:port.
|
|
||||||
func parseHostQueryListen(listen string) (network string, addr string, err error) {
|
func parseHostQueryListen(listen string) (network string, addr string, err error) {
|
||||||
if path, ok := strings.CutPrefix(listen, "unix://"); ok {
|
if path, ok := strings.CutPrefix(listen, "unix://"); ok {
|
||||||
if !filepath.IsAbs(path) {
|
if !filepath.IsAbs(path) {
|
||||||
|
|||||||
+20
-20
@@ -56,17 +56,17 @@ func Test_parseHostQueryListen(t *testing.T) {
|
|||||||
func Test_loadHostQueryConfig(t *testing.T) {
|
func Test_loadHostQueryConfig(t *testing.T) {
|
||||||
c := config.NewC(nil)
|
c := config.NewC(nil)
|
||||||
|
|
||||||
// Absent section: disabled, no error.
|
// absent section means disabled, no error
|
||||||
cfg, err := loadHostQueryConfig(c)
|
cfg, err := loadHostQueryConfig(c)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, cfg.enabled)
|
assert.False(t, cfg.enabled)
|
||||||
|
|
||||||
// Enabled without a listen address is an error.
|
// enabled without a listen address is an error
|
||||||
setHostQueryConfig(c, true, "", "")
|
setHostQueryConfig(c, true, "", "")
|
||||||
_, err = loadHostQueryConfig(c)
|
_, err = loadHostQueryConfig(c)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// Unix socket gets the default mode.
|
// a unix socket gets the default mode
|
||||||
setHostQueryConfig(c, true, "unix:///tmp/hq.sock", "")
|
setHostQueryConfig(c, true, "unix:///tmp/hq.sock", "")
|
||||||
cfg, err = loadHostQueryConfig(c)
|
cfg, err = loadHostQueryConfig(c)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -83,7 +83,7 @@ func Test_loadHostQueryConfig(t *testing.T) {
|
|||||||
_, err = loadHostQueryConfig(c)
|
_, err = loadHostQueryConfig(c)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
|
|
||||||
// Mode bits beyond the permission bits are rejected.
|
// mode bits beyond the permission bits are rejected
|
||||||
setHostQueryConfig(c, true, "unix:///tmp/hq.sock", "10600")
|
setHostQueryConfig(c, true, "unix:///tmp/hq.sock", "10600")
|
||||||
_, err = loadHostQueryConfig(c)
|
_, err = loadHostQueryConfig(c)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -117,8 +117,8 @@ func newTestHostQueryServer(t *testing.T) (*hostQueryServer, *config.C) {
|
|||||||
return h, config.NewC(nil)
|
return h, config.NewC(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// addTestPeer creates a certificate for a peer owning each addr (as a /24 or
|
// addTestPeer creates a certificate for a peer owning each addr (as a /24 or /64) and inserts it
|
||||||
// /64) and inserts it into the hostmap as an established tunnel.
|
// into the hostmap as an established tunnel
|
||||||
func addTestPeer(t *testing.T, hm *HostMap, name string, addrs []netip.Addr, unsafeNetworks []netip.Prefix, groups []string) cert.Certificate {
|
func addTestPeer(t *testing.T, hm *HostMap, name string, addrs []netip.Addr, unsafeNetworks []netip.Prefix, groups []string) cert.Certificate {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
networks := make([]netip.Prefix, 0, len(addrs))
|
networks := make([]netip.Prefix, 0, len(addrs))
|
||||||
@@ -173,7 +173,7 @@ func TestHostQueryServer_handleHost(t *testing.T) {
|
|||||||
[]netip.Prefix{netip.MustParsePrefix("192.168.50.0/24")}, []string{"eng", "ssh"})
|
[]netip.Prefix{netip.MustParsePrefix("192.168.50.0/24")}, []string{"eng", "ssh"})
|
||||||
addTestPeer(t, h.hostMap, "groupless", []netip.Addr{netip.MustParseAddr("10.0.0.77")}, nil, nil)
|
addTestPeer(t, h.hostMap, "groupless", []netip.Addr{netip.MustParseAddr("10.0.0.77")}, nil, nil)
|
||||||
|
|
||||||
// An established peer comes back with its full identity.
|
// an established peer comes back with its full identity
|
||||||
code, body := getHost(t, h, "10.0.0.99")
|
code, body := getHost(t, h, "10.0.0.99")
|
||||||
require.Equal(t, http.StatusOK, code)
|
require.Equal(t, http.StatusOK, code)
|
||||||
assert.Equal(t, "laptop-alice", body["name"])
|
assert.Equal(t, "laptop-alice", body["name"])
|
||||||
@@ -186,7 +186,7 @@ func TestHostQueryServer_handleHost(t *testing.T) {
|
|||||||
assert.NotEmpty(t, body["notBefore"])
|
assert.NotEmpty(t, body["notBefore"])
|
||||||
assert.NotEmpty(t, body["notAfter"])
|
assert.NotEmpty(t, body["notAfter"])
|
||||||
|
|
||||||
// Empty cert slices marshal as [] rather than null.
|
// empty cert slices marshal as [] rather than null
|
||||||
code, body = getHost(t, h, "10.0.0.77")
|
code, body = getHost(t, h, "10.0.0.77")
|
||||||
require.Equal(t, http.StatusOK, code)
|
require.Equal(t, http.StatusOK, code)
|
||||||
require.NotNil(t, body["groups"])
|
require.NotNil(t, body["groups"])
|
||||||
@@ -194,15 +194,15 @@ func TestHostQueryServer_handleHost(t *testing.T) {
|
|||||||
require.NotNil(t, body["unsafeNetworks"])
|
require.NotNil(t, body["unsafeNetworks"])
|
||||||
assert.Empty(t, body["unsafeNetworks"])
|
assert.Empty(t, body["unsafeNetworks"])
|
||||||
|
|
||||||
// A port in addr is ignored so RemoteAddr can be passed through directly,
|
// a port in addr is ignored so RemoteAddr can be passed through directly, including the
|
||||||
// including the bracketed v6 and 4in6 forms.
|
// bracketed v6 and 4in6 forms
|
||||||
for _, q := range []string{"10.0.0.99:54321", "[fd00::99]:443", "::ffff:10.0.0.99"} {
|
for _, q := range []string{"10.0.0.99:54321", "[fd00::99]:443", "::ffff:10.0.0.99"} {
|
||||||
code, body = getHost(t, h, q)
|
code, body = getHost(t, h, q)
|
||||||
require.Equal(t, http.StatusOK, code, "addr=%q", q)
|
require.Equal(t, http.StatusOK, code, "addr=%q", q)
|
||||||
assert.Equal(t, "laptop-alice", body["name"], "addr=%q", q)
|
assert.Equal(t, "laptop-alice", body["name"], "addr=%q", q)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Our own address answers from the local cert state.
|
// our own address answers from the local cert state
|
||||||
code, body = getHost(t, h, "10.0.0.1")
|
code, body = getHost(t, h, "10.0.0.1")
|
||||||
require.Equal(t, http.StatusOK, code)
|
require.Equal(t, http.StatusOK, code)
|
||||||
assert.Equal(t, "self", body["name"])
|
assert.Equal(t, "self", body["name"])
|
||||||
@@ -211,7 +211,7 @@ func TestHostQueryServer_handleHost(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, code)
|
assert.Equal(t, http.StatusNotFound, code)
|
||||||
assert.NotEmpty(t, body["error"])
|
assert.NotEmpty(t, body["error"])
|
||||||
|
|
||||||
// A tunnel mid-teardown (no peer cert) is treated as unknown.
|
// a tunnel mid-teardown (no peer cert) is treated as unknown
|
||||||
h.hostMap.unlockedAddHostInfo(&HostInfo{
|
h.hostMap.unlockedAddHostInfo(&HostInfo{
|
||||||
ConnectionState: &ConnectionState{},
|
ConnectionState: &ConnectionState{},
|
||||||
vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.66")},
|
vpnAddrs: []netip.Addr{netip.MustParseAddr("10.0.0.66")},
|
||||||
@@ -247,7 +247,7 @@ func TestHostQueryServer_handleSelf(t *testing.T) {
|
|||||||
assert.Equal(t, "lighthouse", body["name"])
|
assert.Equal(t, "lighthouse", body["name"])
|
||||||
assert.Equal(t, []any{"10.0.0.1"}, body["vpnAddrs"])
|
assert.Equal(t, []any{"10.0.0.1"}, body["vpnAddrs"])
|
||||||
|
|
||||||
// No cert state available should be an error, not a panic.
|
// no cert state available should be an error, not a panic
|
||||||
h.pki = nil
|
h.pki = nil
|
||||||
w = httptest.NewRecorder()
|
w = httptest.NewRecorder()
|
||||||
h.handleSelf(w, r)
|
h.handleSelf(w, r)
|
||||||
@@ -267,7 +267,7 @@ func unixHTTPClient(path string) *http.Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// waitForServe polls until a GET /v1/self through client succeeds.
|
// waitForServe polls until a GET /v1/self through client succeeds
|
||||||
func waitForServe(t *testing.T, client *http.Client) {
|
func waitForServe(t *testing.T, client *http.Client) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
waitFor(t, func() bool {
|
waitFor(t, func() bool {
|
||||||
@@ -370,7 +370,7 @@ func TestHostQueryServer_staleSocket(t *testing.T) {
|
|||||||
h, _ := newTestHostQueryServer(t)
|
h, _ := newTestHostQueryServer(t)
|
||||||
sock := filepath.Join(t.TempDir(), "hq.sock")
|
sock := filepath.Join(t.TempDir(), "hq.sock")
|
||||||
|
|
||||||
// Simulate an unclean exit: a leftover socket file with no listener.
|
// simulate an unclean exit, a leftover socket file with no listener
|
||||||
stale, err := net.ListenUnix("unix", &net.UnixAddr{Name: sock, Net: "unix"})
|
stale, err := net.ListenUnix("unix", &net.UnixAddr{Name: sock, Net: "unix"})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
stale.SetUnlinkOnClose(false)
|
stale.SetUnlinkOnClose(false)
|
||||||
@@ -407,7 +407,7 @@ func TestHostQueryServer_reload(t *testing.T) {
|
|||||||
sock1 := filepath.Join(dir, "hq1.sock")
|
sock1 := filepath.Join(dir, "hq1.sock")
|
||||||
sock2 := filepath.Join(dir, "hq2.sock")
|
sock2 := filepath.Join(dir, "hq2.sock")
|
||||||
|
|
||||||
// Initial reload only records config; Control.Start launches the runtime.
|
// initial reload only records config, Control.Start is what launches the runtime
|
||||||
setHostQueryConfig(c, false, "unix://"+sock1, "")
|
setHostQueryConfig(c, false, "unix://"+sock1, "")
|
||||||
require.NoError(t, h.reload(c, true))
|
require.NoError(t, h.reload(c, true))
|
||||||
assert.False(t, h.enabled.Load())
|
assert.False(t, h.enabled.Load())
|
||||||
@@ -415,12 +415,12 @@ func TestHostQueryServer_reload(t *testing.T) {
|
|||||||
assert.Nil(t, h.run)
|
assert.Nil(t, h.run)
|
||||||
h.runMu.Unlock()
|
h.runMu.Unlock()
|
||||||
|
|
||||||
// Enabling via reload spawns the listener.
|
// enabling via reload spawns the listener
|
||||||
setHostQueryConfig(c, true, "unix://"+sock1, "")
|
setHostQueryConfig(c, true, "unix://"+sock1, "")
|
||||||
require.NoError(t, h.reload(c, false))
|
require.NoError(t, h.reload(c, false))
|
||||||
waitForServe(t, unixHTTPClient(sock1))
|
waitForServe(t, unixHTTPClient(sock1))
|
||||||
|
|
||||||
// Changing the listen path restarts on the new address.
|
// changing the listen path restarts on the new address
|
||||||
setHostQueryConfig(c, true, "unix://"+sock2, "")
|
setHostQueryConfig(c, true, "unix://"+sock2, "")
|
||||||
require.NoError(t, h.reload(c, false))
|
require.NoError(t, h.reload(c, false))
|
||||||
waitForServe(t, unixHTTPClient(sock2))
|
waitForServe(t, unixHTTPClient(sock2))
|
||||||
@@ -429,7 +429,7 @@ func TestHostQueryServer_reload(t *testing.T) {
|
|||||||
return os.IsNotExist(err)
|
return os.IsNotExist(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Reloading an unchanged config does not restart the runtime.
|
// reloading an unchanged config does not restart the runtime
|
||||||
h.runMu.Lock()
|
h.runMu.Lock()
|
||||||
rt := h.run
|
rt := h.run
|
||||||
h.runMu.Unlock()
|
h.runMu.Unlock()
|
||||||
@@ -438,7 +438,7 @@ func TestHostQueryServer_reload(t *testing.T) {
|
|||||||
assert.Same(t, rt, h.run)
|
assert.Same(t, rt, h.run)
|
||||||
h.runMu.Unlock()
|
h.runMu.Unlock()
|
||||||
|
|
||||||
// Disabling stops the listener.
|
// disabling stops the listener
|
||||||
setHostQueryConfig(c, false, "unix://"+sock2, "")
|
setHostQueryConfig(c, false, "unix://"+sock2, "")
|
||||||
require.NoError(t, h.reload(c, false))
|
require.NoError(t, h.reload(c, false))
|
||||||
assert.False(t, h.enabled.Load())
|
assert.False(t, h.enabled.Load())
|
||||||
|
|||||||
Reference in New Issue
Block a user