mirror of
https://github.com/juanfont/headscale.git
synced 2026-07-12 02:51:12 +09:00
dns, change, noise, auth, capver: misc consolidation
This commit is contained in:
@@ -11,21 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
var colors bool
|
||||
|
||||
switch l := termcolor.SupportLevel(os.Stderr); l {
|
||||
case termcolor.Level16M:
|
||||
colors = true
|
||||
case termcolor.Level256:
|
||||
colors = true
|
||||
case termcolor.LevelBasic:
|
||||
colors = true
|
||||
case termcolor.LevelNone:
|
||||
colors = false
|
||||
default:
|
||||
// no color, return text as is.
|
||||
colors = false
|
||||
}
|
||||
colors := termcolor.SupportLevel(os.Stderr) != termcolor.LevelNone
|
||||
|
||||
// Adhere to no-color.org manifesto of allowing users to
|
||||
// turn off color in cli/services
|
||||
|
||||
+18
-8
@@ -23,6 +23,18 @@ type AuthProvider interface {
|
||||
AuthURL(authID types.AuthID) string
|
||||
}
|
||||
|
||||
// machineKeyMismatch fails closed when a node looked up by NodeKey was started
|
||||
// in a Noise session with a different machine key. Without this anyone holding a
|
||||
// target's NodeKey could open a session with a throwaway machine key and act on
|
||||
// the owner's node. Returns a 401 [HTTPError] on mismatch, nil otherwise.
|
||||
func machineKeyMismatch(node types.NodeView, machineKey key.MachinePublic) error {
|
||||
if node.MachineKey() != machineKey {
|
||||
return NewHTTPError(http.StatusUnauthorized, "node exists with a different machine key", nil)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Headscale) handleRegister(
|
||||
ctx context.Context,
|
||||
req tailcfg.RegisterRequest,
|
||||
@@ -75,12 +87,9 @@ func (h *Headscale) handleRegister(
|
||||
// open a Noise session with a throwaway machine key and read
|
||||
// the owner's User/Login back through [nodeToRegisterResponse].
|
||||
// [Headscale.handleLogout] enforces the same check on its own path.
|
||||
if node.MachineKey() != machineKey {
|
||||
return nil, NewHTTPError(
|
||||
http.StatusUnauthorized,
|
||||
"node exists with a different machine key",
|
||||
nil,
|
||||
)
|
||||
err := machineKeyMismatch(node, machineKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// When tailscaled restarts, it sends [tailcfg.RegisterRequest] with Auth=nil and Expiry=zero.
|
||||
@@ -153,8 +162,9 @@ func (h *Headscale) handleLogout(
|
||||
// Fail closed if it looks like this is an attempt to modify a node where
|
||||
// the node key and the machine key the noise session was started with does
|
||||
// not align.
|
||||
if node.MachineKey() != machineKey {
|
||||
return nil, NewHTTPError(http.StatusUnauthorized, "node exist with different machine key", nil)
|
||||
err := machineKeyMismatch(node, machineKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Note: We do NOT return early if req.Auth is set, because Tailscale clients
|
||||
|
||||
@@ -27,7 +27,7 @@ type ExtraRecordsMan struct {
|
||||
|
||||
updateCh chan []tailcfg.DNSRecord
|
||||
closeCh chan struct{}
|
||||
hashes map[string][32]byte
|
||||
hash [32]byte
|
||||
}
|
||||
|
||||
// NewExtraRecordsManager creates a new [ExtraRecordsMan] and starts watching the file at the given path.
|
||||
@@ -52,12 +52,10 @@ func NewExtraRecordsManager(path string) (*ExtraRecordsMan, error) {
|
||||
}
|
||||
|
||||
er := &ExtraRecordsMan{
|
||||
watcher: watcher,
|
||||
path: path,
|
||||
records: set.SetOf(records),
|
||||
hashes: map[string][32]byte{
|
||||
path: hash,
|
||||
},
|
||||
watcher: watcher,
|
||||
path: path,
|
||||
records: set.SetOf(records),
|
||||
hash: hash,
|
||||
closeCh: make(chan struct{}),
|
||||
updateCh: make(chan []tailcfg.DNSRecord),
|
||||
}
|
||||
@@ -160,18 +158,16 @@ func (e *ExtraRecordsMan) updateRecords() {
|
||||
e.mu.Lock()
|
||||
|
||||
// If there has not been any change, ignore the update.
|
||||
if oldHash, ok := e.hashes[e.path]; ok {
|
||||
if newHash == oldHash {
|
||||
e.mu.Unlock()
|
||||
if newHash == e.hash {
|
||||
e.mu.Unlock()
|
||||
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
oldCount := e.records.Len()
|
||||
|
||||
e.records = set.SetOf(records)
|
||||
e.hashes[e.path] = newHash
|
||||
e.hash = newHash
|
||||
toSend := e.records.Slice()
|
||||
|
||||
log.Trace().Caller().Interface("records", e.records).Msgf("extra records updated from path, count old: %d, new: %d", oldCount, e.records.Len())
|
||||
@@ -190,22 +186,24 @@ func (e *ExtraRecordsMan) updateRecords() {
|
||||
// readExtraRecordsFromPath reads a JSON file of [tailcfg.DNSRecord]
|
||||
// and returns the records and the hash of the file.
|
||||
func readExtraRecordsFromPath(path string) ([]tailcfg.DNSRecord, [32]byte, error) {
|
||||
var zero [32]byte
|
||||
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, [32]byte{}, fmt.Errorf("reading path: %s, err: %w", path, err)
|
||||
return nil, zero, fmt.Errorf("reading path: %s, err: %w", path, err)
|
||||
}
|
||||
|
||||
// If the read was triggered too fast, and the file is not complete, ignore the update
|
||||
// if the file is empty. A consecutive update will be triggered when the file is complete.
|
||||
if len(b) == 0 {
|
||||
return nil, [32]byte{}, nil
|
||||
return nil, zero, nil
|
||||
}
|
||||
|
||||
var records []tailcfg.DNSRecord
|
||||
|
||||
err = json.Unmarshal(b, &records)
|
||||
if err != nil {
|
||||
return nil, [32]byte{}, fmt.Errorf("unmarshalling records, content: %q: %w", string(b), err)
|
||||
return nil, zero, fmt.Errorf("unmarshalling records, content: %q: %w", string(b), err)
|
||||
}
|
||||
|
||||
hash := sha256.Sum256(b)
|
||||
|
||||
@@ -343,7 +343,7 @@ func (a *AuthProviderWeb) AuthHandler(
|
||||
}
|
||||
|
||||
func authIDFromRequest(req *http.Request) (types.AuthID, error) {
|
||||
raw, err := urlParam[string](req, "auth_id")
|
||||
raw, err := stringParam(req, "auth_id")
|
||||
if err != nil {
|
||||
return "", NewHTTPError(http.StatusBadRequest, "invalid auth id", fmt.Errorf("parsing auth_id from URL: %w", err))
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ func (t *testBatcherWrapper) AddNode(id types.NodeID, c chan<- *tailcfg.MapRespo
|
||||
return fmt.Errorf("%w: %d", errNodeNotFoundAfterAdd, id)
|
||||
}
|
||||
|
||||
t.AddWork(change.NodeOnlineFor(node))
|
||||
t.AddWork(change.NodeOnline(node.ID()))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -90,7 +90,7 @@ func (t *testBatcherWrapper) RemoveNode(id types.NodeID, c chan<- *tailcfg.MapRe
|
||||
// Do this BEFORE removing from batcher so the change can be processed
|
||||
node, ok := t.state.GetNodeByID(id)
|
||||
if ok {
|
||||
t.AddWork(change.NodeOfflineFor(node))
|
||||
t.AddWork(change.NodeOffline(node.ID()))
|
||||
}
|
||||
|
||||
// Finally remove from the real batcher
|
||||
|
||||
+16
-32
@@ -31,9 +31,6 @@ var ErrUnsupportedClientVersion = errors.New("unsupported client version")
|
||||
// ErrMissingURLParameter is returned when a required URL parameter is not provided.
|
||||
var ErrMissingURLParameter = errors.New("missing URL parameter")
|
||||
|
||||
// ErrUnsupportedURLParameterType is returned when a URL parameter has an unsupported type.
|
||||
var ErrUnsupportedURLParameterType = errors.New("unsupported URL parameter type")
|
||||
|
||||
// ErrNoAuthSession is returned when an auth_id does not match any active auth session.
|
||||
var ErrNoAuthSession = errors.New("no auth session found")
|
||||
|
||||
@@ -338,40 +335,27 @@ func (h *Headscale) PingResponseHandler(
|
||||
}
|
||||
}
|
||||
|
||||
func urlParam[T any](req *http.Request, key string) (T, error) {
|
||||
var zero T
|
||||
|
||||
func stringParam(req *http.Request, key string) (string, error) {
|
||||
param := chi.URLParam(req, key)
|
||||
if param == "" {
|
||||
return zero, fmt.Errorf("%w: %s", ErrMissingURLParameter, key)
|
||||
return "", fmt.Errorf("%w: %s", ErrMissingURLParameter, key)
|
||||
}
|
||||
|
||||
var value T
|
||||
switch any(value).(type) {
|
||||
case string:
|
||||
v, ok := any(param).(T)
|
||||
if !ok {
|
||||
return zero, fmt.Errorf("%w: %T", ErrUnsupportedURLParameterType, value)
|
||||
}
|
||||
return param, nil
|
||||
}
|
||||
|
||||
value = v
|
||||
case types.NodeID:
|
||||
id, err := types.ParseNodeID(param)
|
||||
if err != nil {
|
||||
return zero, fmt.Errorf("parsing %s: %w", key, err)
|
||||
}
|
||||
|
||||
v, ok := any(id).(T)
|
||||
if !ok {
|
||||
return zero, fmt.Errorf("%w: %T", ErrUnsupportedURLParameterType, value)
|
||||
}
|
||||
|
||||
value = v
|
||||
default:
|
||||
return zero, fmt.Errorf("%w: %T", ErrUnsupportedURLParameterType, value)
|
||||
func nodeIDParam(req *http.Request, key string) (types.NodeID, error) {
|
||||
param := chi.URLParam(req, key)
|
||||
if param == "" {
|
||||
return 0, fmt.Errorf("%w: %s", ErrMissingURLParameter, key)
|
||||
}
|
||||
|
||||
return value, nil
|
||||
id, err := types.ParseNodeID(param)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parsing %s: %w", key, err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// SSHActionHandler handles the /ssh-action endpoint, returning a
|
||||
@@ -381,7 +365,7 @@ func (ns *noiseServer) SSHActionHandler(
|
||||
writer http.ResponseWriter,
|
||||
req *http.Request,
|
||||
) {
|
||||
srcNodeID, err := urlParam[types.NodeID](req, "src_node_id")
|
||||
srcNodeID, err := nodeIDParam(req, "src_node_id")
|
||||
if err != nil {
|
||||
httpError(writer, NewHTTPError(
|
||||
http.StatusBadRequest,
|
||||
@@ -392,7 +376,7 @@ func (ns *noiseServer) SSHActionHandler(
|
||||
return
|
||||
}
|
||||
|
||||
dstNodeID, err := urlParam[types.NodeID](req, "dst_node_id")
|
||||
dstNodeID, err := nodeIDParam(req, "dst_node_id")
|
||||
if err != nil {
|
||||
httpError(writer, NewHTTPError(
|
||||
http.StatusBadRequest,
|
||||
|
||||
@@ -644,7 +644,10 @@ func (s *State) Connect(id types.NodeID) ([]change.Change, uint64) {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
c := []change.Change{change.NodeOnlineFor(node)}
|
||||
// A node coming online sends a lightweight online peer patch. Subnet
|
||||
// routers, relay targets, and via targets get their full peer recompute
|
||||
// from the gated PolicyChange below, so no full update is needed here.
|
||||
c := []change.Change{change.NodeOnline(node.ID())}
|
||||
|
||||
log.Info().EmbedObject(node).Msg("node connected")
|
||||
|
||||
@@ -727,7 +730,10 @@ func (s *State) Disconnect(id types.NodeID, epoch uint64) ([]change.Change, erro
|
||||
// An ordinary node going offline just sends the lightweight offline
|
||||
// patch; emitting a PolicyChange for it would force every peer to
|
||||
// rebuild its netmap on every disconnect.
|
||||
cs := []change.Change{change.NodeOfflineFor(node), c}
|
||||
// A node going offline sends a lightweight offline peer patch. Subnet
|
||||
// routers and other recompute-forcing nodes rely on the gated
|
||||
// PolicyChange below for the peer recompute, so no full update here.
|
||||
cs := []change.Change{change.NodeOffline(node.ID()), c}
|
||||
if s.polMan.NodeNeedsPeerRecompute(node) {
|
||||
cs = append(cs, change.PolicyChange())
|
||||
}
|
||||
|
||||
@@ -456,22 +456,6 @@ func NodeRemoved(id types.NodeID) Change {
|
||||
return PeersRemoved(id)
|
||||
}
|
||||
|
||||
// NodeOnlineFor returns the [Change] for a node coming online: a lightweight
|
||||
// [NodeOnline] peer patch. Subnet routers, relay targets, and via targets get
|
||||
// their full peer recompute from the gated [PolicyChange] that State.Connect
|
||||
// emits, so no full update is needed here.
|
||||
func NodeOnlineFor(node types.NodeView) Change {
|
||||
return NodeOnline(node.ID())
|
||||
}
|
||||
|
||||
// NodeOfflineFor returns the [Change] for a node going offline: a lightweight
|
||||
// [NodeOffline] peer patch. As with [NodeOnlineFor], subnet routers and other
|
||||
// recompute-forcing nodes rely on the gated [PolicyChange] from State.Disconnect
|
||||
// for the peer recompute, so no full update is needed here.
|
||||
func NodeOfflineFor(node types.NodeView) Change {
|
||||
return NodeOffline(node.ID())
|
||||
}
|
||||
|
||||
// KeyExpiryFor returns a [Change] for when a node's key expiry changes.
|
||||
// The [Change.OriginNode] field enables self-update detection by the mapper.
|
||||
func KeyExpiryFor(id types.NodeID, expiry time.Time) Change {
|
||||
|
||||
@@ -632,8 +632,8 @@ func TestNodeOnlineOfflineForSubnetRouter(t *testing.T) {
|
||||
got Change
|
||||
wantOnline bool
|
||||
}{
|
||||
{name: "online", got: NodeOnlineFor(view), wantOnline: true},
|
||||
{name: "offline", got: NodeOfflineFor(view), wantOnline: false},
|
||||
{name: "online", got: NodeOnline(view.ID()), wantOnline: true},
|
||||
{name: "offline", got: NodeOffline(view.ID()), wantOnline: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
+22
-26
@@ -261,6 +261,24 @@ func calculateMinSupportedCapabilityVersion(versions map[string]tailcfg.Capabili
|
||||
return versions[oldestSupportedMinor]
|
||||
}
|
||||
|
||||
// firstTailscaleVerPerCapVer inverts versions into a map from each capability
|
||||
// version to the first (lowest-sorted) Tailscale minor version reporting it.
|
||||
func firstTailscaleVerPerCapVer(versions map[string]tailcfg.CapabilityVersion) map[tailcfg.CapabilityVersion]string {
|
||||
sortedVersions := xmaps.Keys(versions)
|
||||
sort.Strings(sortedVersions)
|
||||
|
||||
capVerToTailscaleVer := make(map[tailcfg.CapabilityVersion]string)
|
||||
|
||||
for _, v := range sortedVersions {
|
||||
capabilityVersion := versions[v]
|
||||
if _, ok := capVerToTailscaleVer[capabilityVersion]; !ok {
|
||||
capVerToTailscaleVer[capabilityVersion] = v
|
||||
}
|
||||
}
|
||||
|
||||
return capVerToTailscaleVer
|
||||
}
|
||||
|
||||
func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion, minSupportedCapVer tailcfg.CapabilityVersion) error {
|
||||
// Generate the Go code as a string
|
||||
var content strings.Builder
|
||||
@@ -282,26 +300,13 @@ func writeCapabilityVersionsToFile(versions map[string]tailcfg.CapabilityVersion
|
||||
content.WriteString("\n\n")
|
||||
content.WriteString("var capVerToTailscaleVer = map[tailcfg.CapabilityVersion]string{\n")
|
||||
|
||||
capVarToTailscaleVer := make(map[tailcfg.CapabilityVersion]string)
|
||||
capVerToTailscaleVer := firstTailscaleVerPerCapVer(versions)
|
||||
|
||||
for _, v := range sortedVersions {
|
||||
capabilityVersion := versions[v]
|
||||
|
||||
// If it is already set, skip and continue,
|
||||
// we only want the first tailscale version per
|
||||
// capability version.
|
||||
if _, ok := capVarToTailscaleVer[capabilityVersion]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
capVarToTailscaleVer[capabilityVersion] = v
|
||||
}
|
||||
|
||||
capsSorted := xmaps.Keys(capVarToTailscaleVer)
|
||||
capsSorted := xmaps.Keys(capVerToTailscaleVer)
|
||||
slices.Sort(capsSorted)
|
||||
|
||||
for _, capVer := range capsSorted {
|
||||
fmt.Fprintf(&content, "\t%d:\t\t\"%s\",\n", capVer, capVarToTailscaleVer[capVer])
|
||||
fmt.Fprintf(&content, "\t%d:\t\t\"%s\",\n", capVer, capVerToTailscaleVer[capVer])
|
||||
}
|
||||
|
||||
content.WriteString("}\n\n")
|
||||
@@ -398,16 +403,7 @@ func writeTestDataFile(versions map[string]tailcfg.CapabilityVersion, minSupport
|
||||
content.WriteString("}\n\n")
|
||||
|
||||
// Build capVerToTailscaleVer for test data
|
||||
capVerToTailscaleVer := make(map[tailcfg.CapabilityVersion]string)
|
||||
sortedVersions := xmaps.Keys(versions)
|
||||
sort.Strings(sortedVersions)
|
||||
|
||||
for _, v := range sortedVersions {
|
||||
capabilityVersion := versions[v]
|
||||
if _, ok := capVerToTailscaleVer[capabilityVersion]; !ok {
|
||||
capVerToTailscaleVer[capabilityVersion] = v
|
||||
}
|
||||
}
|
||||
capVerToTailscaleVer := firstTailscaleVerPerCapVer(versions)
|
||||
|
||||
// Generate complete test struct for [capver.CapVerMinimumTailscaleVersion]
|
||||
content.WriteString("var capVerMinimumTailscaleVersionTests = []struct {\n")
|
||||
|
||||
Reference in New Issue
Block a user