| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- //go:build !ts_omit_tailnetlock
- package ipnlocal
- import (
- "bytes"
- "context"
- "crypto/ed25519"
- "crypto/rand"
- "encoding/base64"
- "encoding/binary"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/netip"
- "os"
- "path/filepath"
- "slices"
- "time"
- "tailscale.com/health"
- "tailscale.com/health/healthmsg"
- "tailscale.com/ipn"
- "tailscale.com/ipn/ipnstate"
- "tailscale.com/net/tsaddr"
- "tailscale.com/tailcfg"
- "tailscale.com/tka"
- "tailscale.com/tsconst"
- "tailscale.com/types/key"
- "tailscale.com/types/logger"
- "tailscale.com/types/netmap"
- "tailscale.com/types/persist"
- "tailscale.com/types/tkatype"
- "tailscale.com/util/mak"
- "tailscale.com/util/set"
- )
- // TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
- var (
- errMissingNetmap = errors.New("missing netmap: verify that you are logged in")
- errNetworkLockNotActive = errors.New("network-lock is not active")
- tkaCompactionDefaults = tka.CompactionOptions{
- MinChain: 24, // Keep at minimum 24 AUMs since head.
- MinAge: 14 * 24 * time.Hour, // Keep 2 weeks of AUMs.
- }
- )
- type tkaState struct {
- profile ipn.ProfileID
- authority *tka.Authority
- storage tka.CompactableChonk
- filtered []ipnstate.TKAPeer
- }
- func (b *LocalBackend) initTKALocked() error {
- cp := b.pm.CurrentProfile()
- if cp.ID() == "" {
- b.tka = nil
- return nil
- }
- if b.tka != nil {
- if b.tka.profile == cp.ID() {
- // Already initialized.
- return nil
- }
- // As we're switching profiles, we need to reset the TKA to nil.
- b.tka = nil
- }
- root := b.TailscaleVarRoot()
- if root == "" {
- b.tka = nil
- b.logf("cannot fetch existing TKA state; no state directory for network-lock")
- return nil
- }
- chonkDir := b.chonkPathLocked()
- if _, err := os.Stat(chonkDir); err == nil {
- // The directory exists, which means network-lock has been initialized.
- storage, err := tka.ChonkDir(chonkDir)
- if err != nil {
- return fmt.Errorf("opening tailchonk: %v", err)
- }
- authority, err := tka.Open(storage)
- if err != nil {
- return fmt.Errorf("initializing tka: %v", err)
- }
- if err := authority.Compact(storage, tkaCompactionDefaults); err != nil {
- b.logf("tka compaction failed: %v", err)
- }
- b.tka = &tkaState{
- profile: cp.ID(),
- authority: authority,
- storage: storage,
- }
- b.logf("tka initialized at head %x", authority.Head())
- }
- return nil
- }
- // noNetworkLockStateDirWarnable is a Warnable to warn the user that Tailnet Lock data
- // (in particular, the list of AUMs in the TKA state) is being stored in memory and will
- // be lost when tailscaled restarts.
- var noNetworkLockStateDirWarnable = health.Register(&health.Warnable{
- Code: "no-tailnet-lock-state-dir",
- Title: "No statedir for Tailnet Lock",
- Severity: health.SeverityMedium,
- Text: health.StaticMessage(healthmsg.InMemoryTailnetLockState),
- })
- // tkaFilterNetmapLocked checks the signatures on each node key, dropping
- // nodes from the netmap whose signature does not verify.
- //
- // b.mu must be held.
- func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
- if b.tka == nil && !b.capTailnetLock {
- b.health.SetTKAHealth(nil)
- return
- }
- if b.tka == nil {
- b.health.SetTKAHealth(nil)
- return // TKA not enabled.
- }
- tracker := rotationTracker{logf: b.logf}
- var toDelete map[int]bool // peer index => true
- for i, p := range nm.Peers {
- if p.UnsignedPeerAPIOnly() {
- // Not subject to tailnet lock.
- continue
- }
- if p.KeySignature().Len() == 0 {
- b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID(), p.StableID())
- mak.Set(&toDelete, i, true)
- } else {
- details, err := b.tka.authority.NodeKeyAuthorizedWithDetails(p.Key(), p.KeySignature().AsSlice())
- if err != nil {
- b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID(), p.StableID(), err)
- mak.Set(&toDelete, i, true)
- continue
- }
- if details != nil {
- // Rotation details are returned when the node key is signed by a valid SigRotation signature.
- tracker.addRotationDetails(p.Key(), details)
- }
- }
- }
- obsoleteByRotation := tracker.obsoleteKeys()
- // nm.Peers is ordered, so deletion must be order-preserving.
- if len(toDelete) > 0 || len(obsoleteByRotation) > 0 {
- peers := make([]tailcfg.NodeView, 0, len(nm.Peers))
- filtered := make([]ipnstate.TKAPeer, 0, len(toDelete)+len(obsoleteByRotation))
- for i, p := range nm.Peers {
- if !toDelete[i] && !obsoleteByRotation.Contains(p.Key()) {
- peers = append(peers, p)
- } else {
- if obsoleteByRotation.Contains(p.Key()) {
- b.logf("Network lock is dropping peer %v(%v) due to key rotation", p.ID(), p.StableID())
- }
- // Record information about the node we filtered out.
- filtered = append(filtered, tkaStateFromPeer(p))
- }
- }
- nm.Peers = peers
- b.tka.filtered = filtered
- } else {
- b.tka.filtered = nil
- }
- // Check that we ourselves are not locked out, report a health issue if so.
- if nm.SelfNode.Valid() && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key(), nm.SelfNode.KeySignature().AsSlice()) != nil {
- b.health.SetTKAHealth(errors.New(healthmsg.LockedOut))
- } else {
- b.health.SetTKAHealth(nil)
- }
- }
- // rotationTracker determines the set of node keys that are made obsolete by key
- // rotation.
- // - for each SigRotation signature, all previous node keys referenced by the
- // nested signatures are marked as obsolete.
- // - if there are multiple SigRotation signatures tracing back to the same
- // wrapping pubkey of the initial SigDirect signature (e.g. if a node is
- // cloned with all its keys), we keep just one of them, marking the others as
- // obsolete.
- type rotationTracker struct {
- // obsolete is the set of node keys that are obsolete due to key rotation.
- // users of rotationTracker should use the obsoleteKeys method for complete results.
- obsolete set.Set[key.NodePublic]
- // byWrappingKey keeps track of rotation details per wrapping pubkey.
- byWrappingKey map[string][]sigRotationDetails
- logf logger.Logf
- }
- // sigRotationDetails holds information about a node key signed by a SigRotation.
- type sigRotationDetails struct {
- np key.NodePublic
- numPrevKeys int
- }
- // addRotationDetails records the rotation signature details for a node key.
- func (r *rotationTracker) addRotationDetails(np key.NodePublic, d *tka.RotationDetails) {
- r.obsolete.Make()
- r.obsolete.AddSlice(d.PrevNodeKeys)
- if d.InitialSig.SigKind != tka.SigDirect {
- // Only enforce uniqueness of chains originating from a SigDirect
- // signature. Chains that begin with a SigCredential can legitimately
- // start from the same wrapping pubkey when multiple nodes join the
- // network using the same reusable auth key.
- return
- }
- rd := sigRotationDetails{
- np: np,
- numPrevKeys: len(d.PrevNodeKeys),
- }
- if r.byWrappingKey == nil {
- r.byWrappingKey = make(map[string][]sigRotationDetails)
- }
- wp := string(d.InitialSig.WrappingPubkey)
- r.byWrappingKey[wp] = append(r.byWrappingKey[wp], rd)
- }
- // obsoleteKeys returns the set of node keys that are obsolete due to key rotation.
- func (r *rotationTracker) obsoleteKeys() set.Set[key.NodePublic] {
- for _, v := range r.byWrappingKey {
- // Do not consider signatures for keys that have been marked as obsolete
- // by another signature.
- v = slices.DeleteFunc(v, func(rd sigRotationDetails) bool {
- return r.obsolete.Contains(rd.np)
- })
- if len(v) == 0 {
- continue
- }
- // If there are multiple rotation signatures with the same wrapping
- // pubkey, we need to decide which one is the "latest", and keep it.
- // The signature with the largest number of previous keys is likely to
- // be the latest.
- slices.SortStableFunc(v, func(a, b sigRotationDetails) int {
- // Sort by decreasing number of previous keys.
- return b.numPrevKeys - a.numPrevKeys
- })
- // If there are several signatures with the same number of previous
- // keys, we cannot determine which one is the latest, so all of them are
- // rejected for safety.
- if len(v) >= 2 && v[0].numPrevKeys == v[1].numPrevKeys {
- r.logf("at least two nodes (%s and %s) have equally valid rotation signatures with the same wrapping pubkey, rejecting", v[0].np, v[1].np)
- for _, rd := range v {
- r.obsolete.Add(rd.np)
- }
- } else {
- // The first key in v is the one with the longest chain of previous
- // keys, so it must be the newest one. Mark all older keys as obsolete.
- for _, rd := range v[1:] {
- r.obsolete.Add(rd.np)
- }
- }
- }
- return r.obsolete
- }
- // tkaSyncIfNeeded examines TKA info reported from the control plane,
- // performing the steps necessary to synchronize local tka state.
- //
- // There are 4 scenarios handled here:
- // - Enablement: nm.TKAEnabled but b.tka == nil
- // ∴ reach out to /machine/tka/bootstrap to get the genesis AUM, then
- // initialize TKA.
- // - Disablement: !nm.TKAEnabled but b.tka != nil
- // ∴ reach out to /machine/tka/bootstrap to read the disablement secret,
- // then verify and clear tka local state.
- // - Sync needed: b.tka.Head != nm.TKAHead
- // ∴ complete multi-step synchronization flow.
- // - Everything up to date: All other cases.
- // ∴ no action necessary.
- //
- // tkaSyncIfNeeded immediately takes b.takeSyncLock which is held throughout,
- // and may take b.mu as required.
- func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsView) error {
- b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
- defer b.tkaSyncLock.Unlock()
- b.mu.Lock() // take mu to protect access to synchronized fields.
- defer b.mu.Unlock()
- if b.tka == nil && !b.capTailnetLock {
- return nil
- }
- isEnabled := b.tka != nil
- wantEnabled := nm.TKAEnabled
- if isEnabled || wantEnabled {
- b.logf("tkaSyncIfNeeded: isEnabled=%t, wantEnabled=%t, head=%v", isEnabled, wantEnabled, nm.TKAHead)
- }
- ourNodeKey, ok := prefs.Persist().PublicNodeKeyOK()
- if !ok {
- return errors.New("tkaSyncIfNeeded: no node key in prefs")
- }
- didJustEnable := false
- if isEnabled != wantEnabled {
- var ourHead tka.AUMHash
- if b.tka != nil {
- ourHead = b.tka.authority.Head()
- }
- // Regardless of whether we are moving to disabled or enabled, we
- // need information from the tka bootstrap endpoint.
- b.mu.Unlock()
- bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
- b.mu.Lock()
- if err != nil {
- return fmt.Errorf("fetching bootstrap: %w", err)
- }
- if wantEnabled && !isEnabled {
- if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM, prefs.Persist()); err != nil {
- return fmt.Errorf("bootstrap: %w", err)
- }
- isEnabled = true
- didJustEnable = true
- } else if !wantEnabled && isEnabled {
- if err := b.tkaApplyDisablementLocked(bs.DisablementSecret); err != nil {
- // We log here instead of returning an error (which itself would be
- // logged), so that sync will continue even if control gives us an
- // incorrect disablement secret.
- b.logf("Disablement failed, leaving TKA enabled. Error: %v", err)
- } else {
- isEnabled = false
- b.health.SetTKAHealth(nil)
- }
- } else {
- return fmt.Errorf("[bug] unreachable invariant of wantEnabled w/ isEnabled")
- }
- }
- // We always transmit the sync RPCs if TKA was just enabled.
- // This informs the control plane that our TKA state is now
- // initialized to the transmitted TKA head hash.
- if isEnabled && (b.tka.authority.Head() != nm.TKAHead || didJustEnable) {
- if err := b.tkaSyncLocked(ourNodeKey); err != nil {
- return fmt.Errorf("tka sync: %w", err)
- }
- // Try to compact the TKA state, to avoid unbounded storage on nodes.
- //
- // We run this on every sync so that clients compact consistently. In many
- // cases this will be a no-op.
- if err := b.tka.authority.Compact(b.tka.storage, tkaCompactionDefaults); err != nil {
- return fmt.Errorf("tka compact: %w", err)
- }
- }
- return nil
- }
- // tkaSyncLocked synchronizes TKA state with control. b.mu must be held
- // and tka must be initialized. b.mu will be stepped out of (and back into)
- // during network RPCs.
- //
- // b.mu must be held.
- func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
- offer, err := b.tka.authority.SyncOffer(b.tka.storage)
- if err != nil {
- return fmt.Errorf("offer: %w", err)
- }
- b.mu.Unlock()
- offerResp, err := b.tkaDoSyncOffer(ourNodeKey, offer)
- b.mu.Lock()
- if err != nil {
- return fmt.Errorf("offer RPC: %w", err)
- }
- controlOffer, err := tka.ToSyncOffer(offerResp.Head, offerResp.Ancestors)
- if err != nil {
- return fmt.Errorf("control offer: %v", err)
- }
- if controlOffer.Head == offer.Head {
- // We are up to date.
- return nil
- }
- // Compute missing AUMs before we apply any AUMs from the control-plane,
- // so we still submit AUMs to control even if they are not part of the
- // active chain.
- toSendAUMs, err := b.tka.authority.MissingAUMs(b.tka.storage, controlOffer)
- if err != nil {
- return fmt.Errorf("computing missing AUMs: %w", err)
- }
- // If we got this far, then we are not up to date. Either the control-plane
- // has updates for us, or we have updates for the control plane.
- //
- // TODO(tom): Do we want to keep processing even if the Inform fails? Need
- // to think through if theres holdback concerns here or not.
- if len(offerResp.MissingAUMs) > 0 {
- aums := make([]tka.AUM, len(offerResp.MissingAUMs))
- for i, a := range offerResp.MissingAUMs {
- if err := aums[i].Unserialize(a); err != nil {
- return fmt.Errorf("MissingAUMs[%d]: %v", i, err)
- }
- }
- if err := b.tka.authority.Inform(b.tka.storage, aums); err != nil {
- return fmt.Errorf("inform failed: %v", err)
- }
- }
- // NOTE(tom): We always send this RPC so control knows what TKA
- // head we landed at.
- head := b.tka.authority.Head()
- b.mu.Unlock()
- sendResp, err := b.tkaDoSyncSend(ourNodeKey, head, toSendAUMs, false)
- b.mu.Lock()
- if err != nil {
- return fmt.Errorf("send RPC: %v", err)
- }
- var remoteHead tka.AUMHash
- if err := remoteHead.UnmarshalText([]byte(sendResp.Head)); err != nil {
- return fmt.Errorf("head unmarshal: %v", err)
- }
- if remoteHead != b.tka.authority.Head() {
- b.logf("TKA desync: expected consensus after sync but our head is %v and the control plane's is %v", b.tka.authority.Head(), remoteHead)
- }
- return nil
- }
- // tkaApplyDisablementLocked checks a disablement secret and locally disables
- // TKA (if correct). An error is returned if disablement failed.
- //
- // b.mu must be held & TKA must be initialized.
- func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
- if b.tka.authority.ValidDisablement(secret) {
- if err := b.tka.storage.RemoveAll(); err != nil {
- return err
- }
- b.tka = nil
- return nil
- }
- return errors.New("incorrect disablement secret")
- }
- // chonkPathLocked returns the absolute path to the directory in which TKA
- // state (the 'tailchonk') is stored.
- //
- // b.mu must be held.
- func (b *LocalBackend) chonkPathLocked() string {
- return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID()))
- }
- // tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
- // tailnet key authority, based on the given genesis AUM.
- //
- // b.mu must be held.
- func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, persist persist.PersistView) error {
- var genesis tka.AUM
- if err := genesis.Unserialize(g); err != nil {
- return fmt.Errorf("reading genesis: %v", err)
- }
- if persist.Valid() && persist.DisallowedTKAStateIDs().Len() > 0 {
- if genesis.State == nil {
- return errors.New("invalid genesis: missing State")
- }
- bootstrapStateID := fmt.Sprintf("%d:%d", genesis.State.StateID1, genesis.State.StateID2)
- for _, stateID := range persist.DisallowedTKAStateIDs().All() {
- if stateID == bootstrapStateID {
- return fmt.Errorf("TKA with stateID of %q is disallowed on this node", stateID)
- }
- }
- }
- root := b.TailscaleVarRoot()
- var storage tka.CompactableChonk
- if root == "" {
- b.health.SetUnhealthy(noNetworkLockStateDirWarnable, nil)
- b.logf("network-lock using in-memory storage; no state directory")
- storage = tka.ChonkMem()
- } else {
- chonkDir := b.chonkPathLocked()
- chonk, err := tka.ChonkDir(chonkDir)
- if err != nil {
- return fmt.Errorf("chonk: %v", err)
- }
- storage = chonk
- }
- authority, err := tka.Bootstrap(storage, genesis)
- if err != nil {
- return fmt.Errorf("tka bootstrap: %v", err)
- }
- b.tka = &tkaState{
- profile: b.pm.CurrentProfile().ID(),
- authority: authority,
- storage: storage,
- }
- return nil
- }
- // NetworkLockStatus returns a structure describing the state of the
- // tailnet key authority, if any.
- func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
- b.mu.Lock()
- defer b.mu.Unlock()
- var (
- nodeKey *key.NodePublic
- nlPriv key.NLPrivate
- )
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
- nkp := p.Persist().PublicNodeKey()
- nodeKey = &nkp
- nlPriv = p.Persist().NetworkLockKey()
- }
- if nlPriv.IsZero() {
- return &ipnstate.NetworkLockStatus{
- Enabled: false,
- NodeKey: nodeKey,
- }
- }
- if b.tka == nil {
- return &ipnstate.NetworkLockStatus{
- Enabled: false,
- NodeKey: nodeKey,
- PublicKey: nlPriv.Public(),
- }
- }
- var head [32]byte
- h := b.tka.authority.Head()
- copy(head[:], h[:])
- var selfAuthorized bool
- nodeKeySignature := &tka.NodeKeySignature{}
- nm := b.currentNode().NetMap()
- if nm != nil {
- selfAuthorized = b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key(), nm.SelfNode.KeySignature().AsSlice()) == nil
- if err := nodeKeySignature.Unserialize(nm.SelfNode.KeySignature().AsSlice()); err != nil {
- b.logf("failed to decode self node key signature: %v", err)
- }
- }
- keys := b.tka.authority.Keys()
- outKeys := make([]ipnstate.TKAKey, len(keys))
- for i, k := range keys {
- outKeys[i] = ipnstate.TKAKey{
- Kind: k.Kind.String(),
- Key: key.NLPublicFromEd25519Unsafe(k.Public),
- Metadata: k.Meta,
- Votes: k.Votes,
- }
- }
- filtered := make([]*ipnstate.TKAPeer, len(b.tka.filtered))
- for i := range len(filtered) {
- filtered[i] = b.tka.filtered[i].Clone()
- }
- var visible []*ipnstate.TKAPeer
- if nm != nil {
- visible = make([]*ipnstate.TKAPeer, len(nm.Peers))
- for i, p := range nm.Peers {
- s := tkaStateFromPeer(p)
- visible[i] = &s
- }
- }
- stateID1, _ := b.tka.authority.StateIDs()
- return &ipnstate.NetworkLockStatus{
- Enabled: true,
- Head: &head,
- PublicKey: nlPriv.Public(),
- NodeKey: nodeKey,
- NodeKeySigned: selfAuthorized,
- NodeKeySignature: nodeKeySignature,
- TrustedKeys: outKeys,
- FilteredPeers: filtered,
- VisiblePeers: visible,
- StateID: stateID1,
- }
- }
- func tkaStateFromPeer(p tailcfg.NodeView) ipnstate.TKAPeer {
- fp := ipnstate.TKAPeer{
- Name: p.Name(),
- ID: p.ID(),
- StableID: p.StableID(),
- TailscaleIPs: make([]netip.Addr, 0, p.Addresses().Len()),
- NodeKey: p.Key(),
- }
- for _, addr := range p.Addresses().All() {
- if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
- fp.TailscaleIPs = append(fp.TailscaleIPs, addr.Addr())
- }
- }
- var decoded tka.NodeKeySignature
- if err := decoded.Unserialize(p.KeySignature().AsSlice()); err == nil {
- fp.NodeKeySignature = decoded
- }
- return fp
- }
- // NetworkLockInit enables network-lock for the tailnet, with the tailnets'
- // key authority initialized to trust the provided keys.
- //
- // Initialization involves two RPCs with control, termed 'begin' and 'finish'.
- // The Begin RPC transmits the genesis Authority Update Message, which
- // encodes the initial state of the authority, and the list of all nodes
- // needing signatures is returned as a response.
- // The Finish RPC submits signatures for all these nodes, at which point
- // Control has everything it needs to atomically enable network lock.
- func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) error {
- var ourNodeKey key.NodePublic
- var nlPriv key.NLPrivate
- b.mu.Lock()
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
- ourNodeKey = p.Persist().PublicNodeKey()
- nlPriv = p.Persist().NetworkLockKey()
- }
- b.mu.Unlock()
- if ourNodeKey.IsZero() || nlPriv.IsZero() {
- return errors.New("no node-key: is tailscale logged in?")
- }
- var entropy [16]byte
- if _, err := rand.Read(entropy[:]); err != nil {
- return err
- }
- // Generates a genesis AUM representing trust in the provided keys.
- // We use an in-memory tailchonk because we don't want to commit to
- // the filesystem until we've finished the initialization sequence,
- // just in case something goes wrong.
- _, genesisAUM, err := tka.Create(tka.ChonkMem(), tka.State{
- Keys: keys,
- // TODO(tom): s/tka.State.DisablementSecrets/tka.State.DisablementValues
- // This will center on consistent nomenclature:
- // - DisablementSecret: value needed to disable.
- // - DisablementValue: the KDF of the disablement secret, a public value.
- DisablementSecrets: disablementValues,
- StateID1: binary.LittleEndian.Uint64(entropy[:8]),
- StateID2: binary.LittleEndian.Uint64(entropy[8:]),
- }, nlPriv)
- if err != nil {
- return fmt.Errorf("tka.Create: %v", err)
- }
- b.logf("Generated genesis AUM to initialize network lock, trusting the following keys:")
- for i, k := range genesisAUM.State.Keys {
- b.logf(" - key[%d] = tlpub:%x with %d votes", i, k.Public, k.Votes)
- }
- // Phase 1/2 of initialization: Transmit the genesis AUM to Control.
- initResp, err := b.tkaInitBegin(ourNodeKey, genesisAUM)
- if err != nil {
- return fmt.Errorf("tka init-begin RPC: %w", err)
- }
- // Our genesis AUM was accepted but before Control turns on enforcement of
- // node-key signatures, we need to sign keys for all the existing nodes.
- // If we don't get these signatures ahead of time, everyone will lose
- // connectivity because control won't have any signatures to send which
- // satisfy network-lock checks.
- sigs := make(map[tailcfg.NodeID]tkatype.MarshaledSignature, len(initResp.NeedSignatures))
- for _, nodeInfo := range initResp.NeedSignatures {
- nks, err := signNodeKey(nodeInfo, nlPriv)
- if err != nil {
- return fmt.Errorf("generating signature: %v", err)
- }
- sigs[nodeInfo.NodeID] = nks.Serialize()
- }
- // Finalize enablement by transmitting signature for all nodes to Control.
- _, err = b.tkaInitFinish(ourNodeKey, sigs, supportDisablement)
- return err
- }
- // NetworkLockAllowed reports whether the node is allowed to use Tailnet Lock.
- func (b *LocalBackend) NetworkLockAllowed() bool {
- b.mu.Lock()
- defer b.mu.Unlock()
- return b.capTailnetLock
- }
- // Only use is in tests.
- func (b *LocalBackend) NetworkLockVerifySignatureForTest(nks tkatype.MarshaledSignature, nodeKey key.NodePublic) error {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return errNetworkLockNotActive
- }
- return b.tka.authority.NodeKeyAuthorized(nodeKey, nks)
- }
- // Only use is in tests.
- func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- panic("network lock not initialized")
- }
- return b.tka.authority.KeyTrusted(keyID)
- }
- // NetworkLockForceLocalDisable shuts down TKA locally, and denylists the current
- // TKA from being initialized locally in future.
- func (b *LocalBackend) NetworkLockForceLocalDisable() error {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return errNetworkLockNotActive
- }
- id1, id2 := b.tka.authority.StateIDs()
- stateID := fmt.Sprintf("%d:%d", id1, id2)
- cn := b.currentNode()
- newPrefs := b.pm.CurrentPrefs().AsStruct().Clone() // .Persist should always be initialized here.
- newPrefs.Persist.DisallowedTKAStateIDs = append(newPrefs.Persist.DisallowedTKAStateIDs, stateID)
- if err := b.pm.SetPrefs(newPrefs.View(), cn.NetworkProfile()); err != nil {
- return fmt.Errorf("saving prefs: %w", err)
- }
- if err := b.tka.storage.RemoveAll(); err != nil {
- return fmt.Errorf("deleting TKA state: %w", err)
- }
- b.tka = nil
- return nil
- }
- // NetworkLockSign signs the given node-key and submits it to the control plane.
- // rotationPublic, if specified, must be an ed25519 public key.
- func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []byte) error {
- ourNodeKey, sig, err := func(nodeKey key.NodePublic, rotationPublic []byte) (key.NodePublic, tka.NodeKeySignature, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- var nlPriv key.NLPrivate
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
- nlPriv = p.Persist().NetworkLockKey()
- }
- if nlPriv.IsZero() {
- return key.NodePublic{}, tka.NodeKeySignature{}, errMissingNetmap
- }
- if b.tka == nil {
- return key.NodePublic{}, tka.NodeKeySignature{}, errNetworkLockNotActive
- }
- if !b.tka.authority.KeyTrusted(nlPriv.KeyID()) {
- return key.NodePublic{}, tka.NodeKeySignature{}, errors.New(tsconst.TailnetLockNotTrustedMsg)
- }
- p, err := nodeKey.MarshalBinary()
- if err != nil {
- return key.NodePublic{}, tka.NodeKeySignature{}, err
- }
- sig := tka.NodeKeySignature{
- SigKind: tka.SigDirect,
- KeyID: nlPriv.KeyID(),
- Pubkey: p,
- WrappingPubkey: rotationPublic,
- }
- sig.Signature, err = nlPriv.SignNKS(sig.SigHash())
- if err != nil {
- return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err)
- }
- return b.pm.CurrentPrefs().Persist().PublicNodeKey(), sig, nil
- }(nodeKey, rotationPublic)
- if err != nil {
- return err
- }
- b.logf("Generated network-lock signature for %v, submitting to control plane", nodeKey)
- if _, err := b.tkaSubmitSignature(ourNodeKey, sig.Serialize()); err != nil {
- return err
- }
- return nil
- }
- // NetworkLockModify adds and/or removes keys in the tailnet's key authority.
- func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err error) {
- defer func() {
- if err != nil {
- err = fmt.Errorf("modify network-lock keys: %w", err)
- }
- }()
- b.mu.Lock()
- defer b.mu.Unlock()
- var ourNodeKey key.NodePublic
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
- ourNodeKey = p.Persist().PublicNodeKey()
- }
- if ourNodeKey.IsZero() {
- return errors.New("no node-key: is tailscale logged in?")
- }
- var nlPriv key.NLPrivate
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
- nlPriv = p.Persist().NetworkLockKey()
- }
- if nlPriv.IsZero() {
- return errMissingNetmap
- }
- if b.tka == nil {
- return errNetworkLockNotActive
- }
- if !b.tka.authority.KeyTrusted(nlPriv.KeyID()) {
- return errors.New("this node does not have a trusted tailnet lock key")
- }
- updater := b.tka.authority.NewUpdater(nlPriv)
- for _, addKey := range addKeys {
- if err := updater.AddKey(addKey); err != nil {
- return err
- }
- }
- for _, removeKey := range removeKeys {
- keyID, err := removeKey.ID()
- if err != nil {
- return err
- }
- if err := updater.RemoveKey(keyID); err != nil {
- return err
- }
- }
- aums, err := updater.Finalize(b.tka.storage)
- if err != nil {
- return err
- }
- if len(aums) == 0 {
- return nil
- }
- head := b.tka.authority.Head()
- b.mu.Unlock()
- resp, err := b.tkaDoSyncSend(ourNodeKey, head, aums, true)
- b.mu.Lock()
- if err != nil {
- return err
- }
- var controlHead tka.AUMHash
- if err := controlHead.UnmarshalText([]byte(resp.Head)); err != nil {
- return err
- }
- lastHead := aums[len(aums)-1].Hash()
- if controlHead != lastHead {
- return errors.New("central tka head differs from submitted AUM, try again")
- }
- return nil
- }
- // NetworkLockDisable disables network-lock using the provided disablement secret.
- func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
- var (
- ourNodeKey key.NodePublic
- head tka.AUMHash
- err error
- )
- b.mu.Lock()
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
- ourNodeKey = p.Persist().PublicNodeKey()
- }
- if b.tka == nil {
- err = errNetworkLockNotActive
- } else {
- head = b.tka.authority.Head()
- if !b.tka.authority.ValidDisablement(secret) {
- err = errors.New("incorrect disablement secret")
- }
- }
- b.mu.Unlock()
- if err != nil {
- return err
- }
- if ourNodeKey.IsZero() {
- return errors.New("no node-key: is tailscale logged in?")
- }
- _, err = b.tkaDoDisablement(ourNodeKey, head, secret)
- return err
- }
- // NetworkLockLog returns the changelog of TKA state up to maxEntries in size.
- func (b *LocalBackend) NetworkLockLog(maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return nil, errNetworkLockNotActive
- }
- var out []ipnstate.NetworkLockUpdate
- cursor := b.tka.authority.Head()
- for range maxEntries {
- aum, err := b.tka.storage.AUM(cursor)
- if err != nil {
- if err == os.ErrNotExist {
- break
- }
- return out, fmt.Errorf("reading AUM (%v): %w", cursor, err)
- }
- update := ipnstate.NetworkLockUpdate{
- Hash: cursor,
- Change: aum.MessageKind.String(),
- Raw: aum.Serialize(),
- }
- out = append(out, update)
- parent, hasParent := aum.Parent()
- if !hasParent {
- break
- }
- cursor = parent
- }
- return out, nil
- }
- // NetworkLockAffectedSigs returns the signatures which would be invalidated
- // by removing trust in the specified KeyID.
- func (b *LocalBackend) NetworkLockAffectedSigs(keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
- var (
- ourNodeKey key.NodePublic
- err error
- )
- b.mu.Lock()
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
- ourNodeKey = p.Persist().PublicNodeKey()
- }
- if b.tka == nil {
- err = errNetworkLockNotActive
- }
- b.mu.Unlock()
- if err != nil {
- return nil, err
- }
- resp, err := b.tkaReadAffectedSigs(ourNodeKey, keyID)
- if err != nil {
- return nil, err
- }
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return nil, errNetworkLockNotActive
- }
- // Confirm for ourselves tha the signatures would actually be invalidated
- // by removal of trusted in the specified key.
- for i, sigBytes := range resp.Signatures {
- var sig tka.NodeKeySignature
- if err := sig.Unserialize(sigBytes); err != nil {
- return nil, fmt.Errorf("failed decoding signature %d: %w", i, err)
- }
- sigKeyID, err := sig.UnverifiedAuthorizingKeyID()
- if err != nil {
- return nil, fmt.Errorf("extracting SigID from signature %d: %w", i, err)
- }
- if !bytes.Equal(keyID, sigKeyID) {
- return nil, fmt.Errorf("got signature with keyID %X from request for %X", sigKeyID, keyID)
- }
- var nodeKey key.NodePublic
- if err := nodeKey.UnmarshalBinary(sig.Pubkey); err != nil {
- return nil, fmt.Errorf("failed decoding pubkey for signature %d: %w", i, err)
- }
- if err := b.tka.authority.NodeKeyAuthorized(nodeKey, sigBytes); err != nil {
- return nil, fmt.Errorf("signature %d is not valid: %w", i, err)
- }
- }
- return resp.Signatures, nil
- }
- // NetworkLockGenerateRecoveryAUM generates an AUM which retroactively removes trust in the
- // specified keys. This AUM is signed by the current node and returned.
- //
- // If forkFrom is specified, it is used as the parent AUM to fork from. If the zero value,
- // the parent AUM is determined automatically.
- func (b *LocalBackend) NetworkLockGenerateRecoveryAUM(removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) (*tka.AUM, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return nil, errNetworkLockNotActive
- }
- var nlPriv key.NLPrivate
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
- nlPriv = p.Persist().NetworkLockKey()
- }
- if nlPriv.IsZero() {
- return nil, errMissingNetmap
- }
- aum, err := b.tka.authority.MakeRetroactiveRevocation(b.tka.storage, removeKeys, nlPriv.KeyID(), forkFrom)
- if err != nil {
- return nil, err
- }
- // Sign it ourselves.
- aum.Signatures, err = nlPriv.SignAUM(aum.SigHash())
- if err != nil {
- return nil, fmt.Errorf("signing failed: %w", err)
- }
- return aum, nil
- }
- // NetworkLockCosignRecoveryAUM co-signs the provided recovery AUM and returns
- // the updated structure.
- //
- // The recovery AUM provided should be the output from a previous call to
- // NetworkLockGenerateRecoveryAUM or NetworkLockCosignRecoveryAUM.
- func (b *LocalBackend) NetworkLockCosignRecoveryAUM(aum *tka.AUM) (*tka.AUM, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return nil, errNetworkLockNotActive
- }
- var nlPriv key.NLPrivate
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() {
- nlPriv = p.Persist().NetworkLockKey()
- }
- if nlPriv.IsZero() {
- return nil, errMissingNetmap
- }
- for _, sig := range aum.Signatures {
- if bytes.Equal(sig.KeyID, nlPriv.KeyID()) {
- return nil, errors.New("this node has already signed this recovery AUM")
- }
- }
- // Sign it ourselves.
- sigs, err := nlPriv.SignAUM(aum.SigHash())
- if err != nil {
- return nil, fmt.Errorf("signing failed: %w", err)
- }
- aum.Signatures = append(aum.Signatures, sigs...)
- return aum, nil
- }
- func (b *LocalBackend) NetworkLockSubmitRecoveryAUM(aum *tka.AUM) error {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return errNetworkLockNotActive
- }
- var ourNodeKey key.NodePublic
- if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
- ourNodeKey = p.Persist().PublicNodeKey()
- }
- if ourNodeKey.IsZero() {
- return errors.New("no node-key: is tailscale logged in?")
- }
- b.mu.Unlock()
- _, err := b.tkaDoSyncSend(ourNodeKey, aum.Hash(), []tka.AUM{*aum}, false)
- b.mu.Lock()
- return err
- }
- var tkaSuffixEncoder = base64.RawStdEncoding
- // NetworkLockWrapPreauthKey wraps a pre-auth key with information to
- // enable unattended bringup in the locked tailnet.
- //
- // The provided trusted tailnet-lock key is used to sign
- // a SigCredential structure, which is encoded along with the
- // private key and appended to the pre-auth key.
- func (b *LocalBackend) NetworkLockWrapPreauthKey(preauthKey string, tkaKey key.NLPrivate) (string, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return "", errNetworkLockNotActive
- }
- pub, priv, err := ed25519.GenerateKey(nil) // nil == crypto/rand
- if err != nil {
- return "", err
- }
- sig := tka.NodeKeySignature{
- SigKind: tka.SigCredential,
- KeyID: tkaKey.KeyID(),
- WrappingPubkey: pub,
- }
- sig.Signature, err = tkaKey.SignNKS(sig.SigHash())
- if err != nil {
- return "", fmt.Errorf("signing failed: %w", err)
- }
- b.logf("Generated network-lock credential signature using %s", tkaKey.Public().CLIString())
- return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString(sig.Serialize()), tkaSuffixEncoder.EncodeToString(priv)), nil
- }
- // NetworkLockVerifySigningDeeplink asks the authority to verify the given deeplink
- // URL. See the comment for ValidateDeeplink for details.
- func (b *LocalBackend) NetworkLockVerifySigningDeeplink(url string) tka.DeeplinkValidationResult {
- b.mu.Lock()
- defer b.mu.Unlock()
- if b.tka == nil {
- return tka.DeeplinkValidationResult{IsValid: false, Error: errNetworkLockNotActive.Error()}
- }
- return b.tka.authority.ValidateDeeplink(url)
- }
- func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
- p, err := nodeInfo.NodePublic.MarshalBinary()
- if err != nil {
- return nil, err
- }
- sig := tka.NodeKeySignature{
- SigKind: tka.SigDirect,
- KeyID: signer.KeyID(),
- Pubkey: p,
- WrappingPubkey: nodeInfo.RotationPubkey,
- }
- sig.Signature, err = signer.SignNKS(sig.SigHash())
- if err != nil {
- return nil, fmt.Errorf("signature failed: %w", err)
- }
- return &sig, nil
- }
- func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- GenesisAUM: aum.Serialize(),
- }); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKAInitBeginResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature, supportDisablement []byte) (*tailcfg.TKAInitFinishResponse, error) {
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- Signatures: nks,
- SupportDisablement: supportDisablement,
- }); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKAInitFinishResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- // tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
- // over noise. This is used to get values necessary to enable or disable TKA.
- func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
- bootstrapReq := tailcfg.TKABootstrapRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- }
- if !head.IsZero() {
- head, err := head.MarshalText()
- if err != nil {
- return nil, fmt.Errorf("head.MarshalText failed: %v", err)
- }
- bootstrapReq.Head = string(head)
- }
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(bootstrapReq); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- if err := ctx.Err(); err != nil {
- return nil, fmt.Errorf("ctx: %w", err)
- }
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/bootstrap", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKABootstrapResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- // tkaDoSyncOffer sends a /machine/tka/sync/offer RPC to the control plane
- // over noise. This is the first of two RPCs implementing tka synchronization.
- func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncOffer) (*tailcfg.TKASyncOfferResponse, error) {
- head, ancestors, err := tka.FromSyncOffer(offer)
- if err != nil {
- return nil, fmt.Errorf("encoding offer: %v", err)
- }
- syncReq := tailcfg.TKASyncOfferRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- Head: head,
- Ancestors: ancestors,
- }
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(syncReq); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/offer", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKASyncOfferResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- // tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane
- // over noise. This is the second of two RPCs implementing tka synchronization.
- func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, head tka.AUMHash, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) {
- headBytes, err := head.MarshalText()
- if err != nil {
- return nil, fmt.Errorf("head.MarshalText: %w", err)
- }
- sendReq := tailcfg.TKASyncSendRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- Head: string(headBytes),
- MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
- Interactive: interactive,
- }
- for i, a := range aums {
- sendReq.MissingAUMs[i] = a.Serialize()
- }
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(sendReq); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/send", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKASyncSendResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- func (b *LocalBackend) tkaDoDisablement(ourNodeKey key.NodePublic, head tka.AUMHash, secret []byte) (*tailcfg.TKADisableResponse, error) {
- headBytes, err := head.MarshalText()
- if err != nil {
- return nil, fmt.Errorf("head.MarshalText: %w", err)
- }
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(tailcfg.TKADisableRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- Head: string(headBytes),
- DisablementSecret: secret,
- }); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/disable", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKADisableResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- func (b *LocalBackend) tkaSubmitSignature(ourNodeKey key.NodePublic, sig tkatype.MarshaledSignature) (*tailcfg.TKASubmitSignatureResponse, error) {
- var req bytes.Buffer
- if err := json.NewEncoder(&req).Encode(tailcfg.TKASubmitSignatureRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- Signature: sig,
- }); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sign", &req)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- res, err := b.DoNoiseRequest(req2)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if res.StatusCode != 200 {
- body, _ := io.ReadAll(res.Body)
- res.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
- }
- a := new(tailcfg.TKASubmitSignatureResponse)
- err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
- res.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
- func (b *LocalBackend) tkaReadAffectedSigs(ourNodeKey key.NodePublic, key tkatype.KeyID) (*tailcfg.TKASignaturesUsingKeyResponse, error) {
- var encodedReq bytes.Buffer
- if err := json.NewEncoder(&encodedReq).Encode(tailcfg.TKASignaturesUsingKeyRequest{
- Version: tailcfg.CurrentCapabilityVersion,
- NodeKey: ourNodeKey,
- KeyID: key,
- }); err != nil {
- return nil, fmt.Errorf("encoding request: %v", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
- defer cancel()
- req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/affected-sigs", &encodedReq)
- if err != nil {
- return nil, fmt.Errorf("req: %w", err)
- }
- resp, err := b.DoNoiseRequest(req)
- if err != nil {
- return nil, fmt.Errorf("resp: %w", err)
- }
- if resp.StatusCode != 200 {
- body, _ := io.ReadAll(resp.Body)
- resp.Body.Close()
- return nil, fmt.Errorf("request returned (%d): %s", resp.StatusCode, string(body))
- }
- a := new(tailcfg.TKASignaturesUsingKeyResponse)
- err = json.NewDecoder(&io.LimitedReader{R: resp.Body, N: 1024 * 1024}).Decode(a)
- resp.Body.Close()
- if err != nil {
- return nil, fmt.Errorf("decoding JSON: %w", err)
- }
- return a, nil
- }
|