Compare commits

...

8 Commits

Author SHA1 Message Date
Anton Tolchanov
c79c500c7e VERSION.txt: this is v1.68.2
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-07-02 19:23:20 +01:00
Jonathan Nobels
0629929368 net/dns: recheck DNS config on SERVFAIL errors (#12547)
Fixes tailscale/corp#20677

Replaces the original attempt to rectify this (by injecting a netMon
event) which was both heavy handed, and missed cases where the
netMon event was "minor".

On apple platforms, the fetching the interface's nameservers can
and does return an empty list in certain situations. Apple's API
in particular is very limiting here. The header hints at notifications
for dns changes which would let us react ahead of time, but it's all
private APIs.

To avoid remaining in the state where we end up with no
nameservers but we absolutely need them, we'll react
to a lack of upstream nameservers by attempting to re-query
the OS.

We'll rate limit this to space out the attempts. It seems relatively
harmless to attempt a reconfig every 5 seconds (triggered
by an incoming query) if the network is in this broken state.

Missing nameservers might possibly be a persistent condition
(vs a transient error), but that would also imply that something
out of our control is badly misconfigured.

Tested by randomly returning [] for the nameservers. When switching
between Wifi networks, or cell->wifi, this will randomly trigger
the bug, and we appear to reliably heal the DNS state.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2024-07-02 19:18:47 +01:00
Anton Tolchanov
1b92ce10bc ipn/ipnlocal: allow multiple signature chains from the same SigCredential
Detection of duplicate Network Lock signature chains added in
01847e0123 failed to account for chains
originating with a SigCredential signature, which is used for wrapped
auth keys. This results in erroneous removal of signatures that
originate from the same re-usable auth key.

This change ensures that multiple nodes created by the same re-usable
auth key are not getting filtered out by the network lock.

Updates tailscale/corp#19764

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-07-02 18:59:24 +01:00
Anton Tolchanov
db1691f8ad tka: test SigCredential signatures and netmap filtering
This change moves handling of wrapped auth keys to the `tka` package and
adds a test covering auth key originating signatures (SigCredential) in
netmap.

Updates tailscale/corp#19764

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
2024-07-02 18:59:24 +01:00
Percy Wegmann
92eacec73f VERSION.txt: this is v1.68.1
Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-06-14 06:47:24 -05:00
Percy Wegmann
a334efa41e ssh/tailssh: check IsSELinuxEnforcing in tailscaled process
Checking in the incubator as this used to do fails because
the getenforce command is not on the PATH.

Updates #12442

Signed-off-by: Percy Wegmann <percy@tailscale.com>
(cherry picked from commit d7fdc01f7f)
2024-06-13 12:14:15 -05:00
Irbe Krumina
87a6138de9 wgengine/netstack: fix 4via6 subnet routes (#12454) (#12455)
Fix a bug where, for a subnet router that advertizes
4via6 route, all packets with a source IP matching
the 4via6 address were being sent to the host itself.
Instead, only send to host packets whose destination
address is host's local address.

Fixes tailscale/tailscale#12448

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
Co-authored-by: Andrew Dunham <andrew@du.nham.ca>
(cherry picked from commit 88f2d234a4)
2024-06-13 18:04:43 +01:00
Mario Minardi
52ddf0d016 VERSION.txt: this is v1.68.0
Signed-off-by: Mario Minardi <mario@tailscale.com>
2024-06-12 11:03:59 -06:00
13 changed files with 316 additions and 145 deletions

View File

@@ -1 +1 @@
1.67.0
1.68.2

View File

@@ -7,8 +7,6 @@ import (
"bufio"
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
@@ -473,7 +471,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
tryingNewKey := c.tryingNewKey
serverKey := c.serverLegacyKey
serverNoiseKey := c.serverNoiseKey
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
authKey, isWrapped, wrappedSig, wrappedKey := tka.DecodeWrappedAuthkey(c.authKey, c.logf)
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
expired := !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
@@ -565,18 +563,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
// We were given a wrapped pre-auth key, which means that in addition
// to being a regular pre-auth key there was a suffix with information to
// generate a tailnet-lock signature.
nk, err := tryingNewKey.Public().MarshalBinary()
nodeKeySignature, err = tka.SignByCredential(wrappedKey, wrappedSig, tryingNewKey.Public())
if err != nil {
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
return false, "", nil, err
}
sig := &tka.NodeKeySignature{
SigKind: tka.SigRotation,
Pubkey: nk,
Nested: wrappedSig,
}
sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(wrappedKey, sigHash[:])
nodeKeySignature = sig.Serialize()
}
if backendLogID == "" {
@@ -1620,43 +1610,6 @@ func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
res.Body.Close()
}
// decodeWrappedAuthkey separates wrapping information from an authkey, if any.
// In all cases the authkey is returned, sans wrapping information if any.
//
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
// and private key.
func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) {
authKey, suffix, found := strings.Cut(key, "--TL")
if !found {
return key, false, nil, nil
}
sigBytes, privBytes, found := strings.Cut(suffix, "-")
if !found {
logf("decoding wrapped auth-key: did not find delimiter")
return key, false, nil, nil
}
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
if err != nil {
logf("decoding wrapped auth-key: signature decode: %v", err)
return key, false, nil, nil
}
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
if err != nil {
logf("decoding wrapped auth-key: priv decode: %v", err)
return key, false, nil, nil
}
sig = new(tka.NodeKeySignature)
if err := sig.Unserialize([]byte(rawSig)); err != nil {
logf("decoding wrapped auth-key: signature: %v", err)
return key, false, nil, nil
}
priv = ed25519.PrivateKey(rawPriv)
return authKey, true, sig, priv
}
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
if !nodeKey.IsZero() {
req.Header.Add(tailcfg.LBHeader, nodeKey.String())

View File

@@ -4,7 +4,6 @@
package controlclient
import (
"crypto/ed25519"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -147,42 +146,3 @@ func TestTsmpPing(t *testing.T) {
t.Fatal(err)
}
}
func TestDecodeWrappedAuthkey(t *testing.T) {
k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
}
if isWrapped {
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
}
if sig != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
}
if priv != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
}
k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
}
if !isWrapped {
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
}
if sig == nil {
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
}
sigHash := sig.SigHash()
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
t.Error("signature failed to verify")
}
// Make sure the private is correct by using it.
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
t.Error("failed to use priv")
}
}

View File

@@ -142,8 +142,9 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
// - 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 (e.g. if a node is cloned with all its keys), we keep
// just one of them, marking the others as obsolete.
// 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.
@@ -165,6 +166,13 @@ type sigRotationDetails struct {
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),
@@ -172,7 +180,7 @@ func (r *rotationTracker) addRotationDetails(np key.NodePublic, d *tka.RotationD
if r.byWrappingKey == nil {
r.byWrappingKey = make(map[string][]sigRotationDetails)
}
wp := string(d.WrappingPubkey)
wp := string(d.InitialSig.WrappingPubkey)
r.byWrappingKey[wp] = append(r.byWrappingKey[wp], rd)
}

View File

@@ -556,6 +556,11 @@ func TestTKAFilterNetmap(t *testing.T) {
t.Fatalf("tka.Create() failed: %v", err)
}
b := &LocalBackend{
logf: t.Logf,
tka: &tkaState{authority: authority},
}
n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode()
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
if err != nil {
@@ -585,6 +590,29 @@ func TestTKAFilterNetmap(t *testing.T) {
n5Rotated, n5RotatedSig := resign(n5nl, n5InitialSig.Serialize())
nodeFromAuthKey := func(authKey string) (key.NodePrivate, tkatype.MarshaledSignature) {
_, isWrapped, sig, priv := tka.DecodeWrappedAuthkey(authKey, t.Logf)
if !isWrapped {
t.Errorf("expected wrapped key")
}
node := key.NewNode()
nodeSig, err := tka.SignByCredential(priv, sig, node.Public())
if err != nil {
t.Error(err)
}
return node, nodeSig
}
preauth, err := b.NetworkLockWrapPreauthKey("tskey-auth-k7UagY1CNTRL-ZZZZZ", nlPriv)
if err != nil {
t.Fatal(err)
}
// Two nodes created using the same auth key, both should be valid.
n60, n60Sig := nodeFromAuthKey(preauth)
n61, n61Sig := nodeFromAuthKey(preauth)
nm := &netmap.NetworkMap{
Peers: nodeViews([]*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
@@ -593,18 +621,18 @@ func TestTKAFilterNetmap(t *testing.T) {
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
{ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
{ID: 60, Key: n60.Public(), KeySignature: n60Sig},
{ID: 61, Key: n61.Public(), KeySignature: n61Sig},
}),
}
b := &LocalBackend{
logf: t.Logf,
tka: &tkaState{authority: authority},
}
b.tkaFilterNetmapLocked(nm)
want := nodeViews([]*tailcfg.Node{
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
{ID: 60, Key: n60.Public(), KeySignature: n60Sig},
{ID: 61, Key: n61.Public(), KeySignature: n61Sig},
})
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
return x.Raw32() == y.Raw32()
@@ -1182,6 +1210,14 @@ func TestRotationTracker(t *testing.T) {
raw32 := [32]byte{idx}
return key.NodePublicFromRaw32(go4mem.B(raw32[:]))
}
rd := func(initialKind tka.SigKind, wrappingKey []byte, prevKeys ...key.NodePublic) *tka.RotationDetails {
return &tka.RotationDetails{
InitialSig: &tka.NodeKeySignature{SigKind: initialKind, WrappingPubkey: wrappingKey},
PrevNodeKeys: prevKeys,
}
}
n1, n2, n3, n4, n5 := newNK(1), newNK(2), newNK(3), newNK(4), newNK(5)
pk1, pk2, pk3 := []byte{1}, []byte{2}, []byte{3}
@@ -1201,46 +1237,46 @@ func TestRotationTracker(t *testing.T) {
{
name: "single_prev_key",
addDetails: []addDetails{
{np: n1, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n2}, WrappingPubkey: pk1}},
{np: n1, details: rd(tka.SigDirect, pk1, n2)},
},
want: set.SetOf([]key.NodePublic{n2}),
},
{
name: "several_prev_keys",
addDetails: []addDetails{
{np: n1, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n2}, WrappingPubkey: pk1}},
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n4}, WrappingPubkey: pk2}},
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n3, n4}, WrappingPubkey: pk1}},
{np: n1, details: rd(tka.SigDirect, pk1, n2)},
{np: n3, details: rd(tka.SigDirect, pk2, n4)},
{np: n2, details: rd(tka.SigDirect, pk1, n3, n4)},
},
want: set.SetOf([]key.NodePublic{n2, n3, n4}),
},
{
name: "several_per_pubkey_latest_wins",
addDetails: []addDetails{
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2, n3}, WrappingPubkey: pk3}},
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n4}, WrappingPubkey: pk3}},
{np: n2, details: rd(tka.SigDirect, pk3, n1)},
{np: n3, details: rd(tka.SigDirect, pk3, n1, n2)},
{np: n4, details: rd(tka.SigDirect, pk3, n1, n2, n3)},
{np: n5, details: rd(tka.SigDirect, pk3, n4)},
},
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4}),
},
{
name: "several_per_pubkey_same_chain_length_all_rejected",
addDetails: []addDetails{
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
{np: n2, details: rd(tka.SigDirect, pk3, n1)},
{np: n3, details: rd(tka.SigDirect, pk3, n1, n2)},
{np: n4, details: rd(tka.SigDirect, pk3, n1, n2)},
{np: n5, details: rd(tka.SigDirect, pk3, n1, n2)},
},
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4, n5}),
},
{
name: "several_per_pubkey_longest_wins",
addDetails: []addDetails{
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2, n3}, WrappingPubkey: pk3}},
{np: n2, details: rd(tka.SigDirect, pk3, n1)},
{np: n3, details: rd(tka.SigDirect, pk3, n1, n2)},
{np: n4, details: rd(tka.SigDirect, pk3, n1, n2)},
{np: n5, details: rd(tka.SigDirect, pk3, n1, n2, n3)},
},
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4}),
},

View File

@@ -14,6 +14,7 @@ import (
"runtime"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
@@ -22,6 +23,8 @@ import (
"tailscale.com/net/dns/resolver"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/syncs"
"tailscale.com/tstime/rate"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
@@ -54,6 +57,12 @@ type Manager struct {
os OSConfigurator
knobs *controlknobs.Knobs // or nil
goos string // if empty, gets set to runtime.GOOS
// The last configuration we successfully compiled. Set to nil if
// there was any failure applying the last configuration
config *Config
// Must be held when accessing/setting config.
mu sync.Mutex
}
// NewManagers created a new manager from the given config.
@@ -79,6 +88,26 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker,
knobs: knobs,
goos: goos,
}
// Rate limit our attempts to correct our DNS configuration.
limiter := rate.NewLimiter(1.0/5.0, 1)
// This will recompile the DNS config, which in turn will requery the system
// DNS settings. The recovery func should triggered only when we are missing
// upstream nameservers and require them to forward a query.
m.resolver.SetMissingUpstreamRecovery(func() {
m.mu.Lock()
defer m.mu.Unlock()
if m.config == nil {
return
}
if limiter.Allow() {
m.logf("DNS resolution failed due to missing upstream nameservers. Recompiling DNS configuration.")
m.setLocked(*m.config)
}
})
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
m.logf("using %T", m.os)
return m
@@ -88,6 +117,19 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, health *health.Tracker,
func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
func (m *Manager) Set(cfg Config) error {
m.mu.Lock()
defer m.mu.Unlock()
return m.setLocked(cfg)
}
// Sets the DNS configuration.
// m.mu must be held
func (m *Manager) setLocked(cfg Config) error {
syncs.AssertLocked(&m.mu)
// On errors, the 'set' config is cleared.
m.config = nil
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
cfg.WriteToBufioWriter(w)
}))
@@ -111,7 +153,9 @@ func (m *Manager) Set(cfg Config) error {
m.health.SetDNSOSHealth(err)
return err
}
m.health.SetDNSOSHealth(nil)
m.config = &cfg
return nil
}

View File

@@ -211,6 +211,10 @@ type forwarder struct {
// /etc/resolv.conf is missing/corrupt, and the peerapi ExitDNS stub
// resolver lookup.
cloudHostFallback []resolverAndDelay
// To be called when a SERVFAIL is returned due to missing upstream resolvers.
// This should attempt to properly (re)set the upstream resolvers.
missingUpstreamRecovery func()
}
func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, knobs *controlknobs.Knobs) *forwarder {
@@ -218,11 +222,12 @@ func newForwarder(logf logger.Logf, netMon *netmon.Monitor, linkSel ForwardLinkS
panic("nil netMon")
}
f := &forwarder{
logf: logger.WithPrefix(logf, "forward: "),
netMon: netMon,
linkSel: linkSel,
dialer: dialer,
controlKnobs: knobs,
logf: logger.WithPrefix(logf, "forward: "),
netMon: netMon,
linkSel: linkSel,
dialer: dialer,
controlKnobs: knobs,
missingUpstreamRecovery: func() {},
}
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
return f
@@ -881,6 +886,12 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
f.logf("no upstream resolvers set, returning SERVFAIL")
// Attempt to recompile the DNS configuration
// If we are being asked to forward queries and we have no
// nameservers, the network is in a bad state.
f.missingUpstreamRecovery()
res, err := servfailResponse(query)
if err != nil {
f.logf("building servfail response: %v", err)

View File

@@ -244,6 +244,13 @@ func New(logf logger.Logf, linkSel ForwardLinkSelector, dialer *tsdial.Dialer, k
return r
}
// Called by the forwarder on SERVFAIL due to missing upstream resolvers
// The func passed in here should attempt to re-query for those resolvers,
// repair, or recover
func (r *Resolver) SetMissingUpstreamRecovery(f func()) {
r.forwarder.missingUpstreamRecovery = f
}
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
func (r *Resolver) SetConfig(cfg Config) error {

View File

@@ -121,6 +121,13 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err
"--tty-name=", // updated in-place by startWithPTY
}
// We have to check the below outside of the incubator process, because it
// relies on the "getenforce" command being on the PATH, which it is not
// when in the incubator.
if runtime.GOOS == "linux" && hostinfo.IsSELinuxEnforcing() {
incubatorArgs = append(incubatorArgs, "--is-selinux-enforcing")
}
forceV1Behavior := ss.conn.srv.lb.NetMap().HasCap(tailcfg.NodeAttrSSHBehaviorV1)
if forceV1Behavior {
incubatorArgs = append(incubatorArgs, "--force-v1-behavior")
@@ -167,20 +174,21 @@ func (stdRWC) Close() error {
}
type incubatorArgs struct {
loginShell string
uid int
gid int
gids []int
localUser string
remoteUser string
remoteIP string
ttyName string
hasTTY bool
cmd string
isSFTP bool
isShell bool
forceV1Behavior bool
debugTest bool
loginShell string
uid int
gid int
gids []int
localUser string
remoteUser string
remoteIP string
ttyName string
hasTTY bool
cmd string
isSFTP bool
isShell bool
forceV1Behavior bool
debugTest bool
isSELinuxEnforcing bool
}
func parseIncubatorArgs(args []string) (incubatorArgs, error) {
@@ -202,6 +210,7 @@ func parseIncubatorArgs(args []string) (incubatorArgs, error) {
flags.BoolVar(&ia.isSFTP, "sftp", false, "run sftp server (cmd is ignored)")
flags.BoolVar(&ia.forceV1Behavior, "force-v1-behavior", false, "allow falling back to the su command if login is unavailable")
flags.BoolVar(&ia.debugTest, "debug-test", false, "should debug in test mode")
flags.BoolVar(&ia.isSELinuxEnforcing, "is-selinux-enforcing", false, "whether SELinux is in enforcing mode")
flags.Parse(args)
for _, g := range strings.Split(groups, ",") {
@@ -338,7 +347,7 @@ func shouldAttemptLoginShell(dlogf logger.Logf, ia incubatorArgs) bool {
return false
}
return runningAsRoot() && !hostinfo.IsSELinuxEnforcing()
return runningAsRoot() && !ia.isSELinuxEnforcing
}
func runningAsRoot() bool {

View File

@@ -31,9 +31,22 @@ RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegra
RUN echo "Then run tests as non-root user testuser and make sure tests still pass."
RUN chown testuser:groupone /tmp/tailscalessh.log
RUN TAILSCALED_PATH=`pwd`tailscaled su -m testuser -c "./tailssh.test -test.v -test.run TestIntegration TestDoDropPrivileges"
RUN chown root:root /tmp/tailscalessh.log
RUN echo "Then run tests in a system that's pretending to be SELinux in enforcing mode"
RUN mv /usr/bin/login /tmp/login_orig
# Use nonsense for /usr/bin/login so that it fails.
# It's not the same failure mode as in SELinux, but failure is good enough for test.
RUN echo "adsfasdfasdf" > /usr/bin/login
RUN chmod 755 /usr/bin/login
# Simulate getenforce command
RUN printf "#!/bin/bash\necho 'Enforcing'" > /usr/bin/getenforce
RUN chmod 755 /usr/bin/getenforce
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegration
RUN mv /tmp/login_orig /usr/bin/login
RUN rm /usr/bin/getenforce
RUN echo "Then remove the login command and make sure tests still pass."
RUN chown root:root /tmp/tailscalessh.log
RUN rm `which login`
RUN rm -Rf /home/testuser
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegrationSFTP

View File

@@ -6,6 +6,7 @@ package tka
import (
"bytes"
"crypto/ed25519"
"encoding/base64"
"errors"
"fmt"
"strings"
@@ -14,6 +15,7 @@ import (
"github.com/hdevalence/ed25519consensus"
"golang.org/x/crypto/blake2s"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/tkatype"
)
@@ -311,9 +313,9 @@ type RotationDetails struct {
// PrevNodeKeys is a list of node keys which have been rotated out.
PrevNodeKeys []key.NodePublic
// WrappingPubkey is the public key which has been authorized to sign
// InitialSig is the first signature in the chain which led to
// this rotating signature.
WrappingPubkey []byte
InitialSig *NodeKeySignature
}
// rotationDetails returns the RotationDetails for a SigRotation signature.
@@ -337,7 +339,7 @@ func (s *NodeKeySignature) rotationDetails() (*RotationDetails, error) {
}
nested = nested.Nested
}
sri.WrappingPubkey = nested.WrappingPubkey
sri.InitialSig = nested
return sri, nil
}
@@ -379,3 +381,64 @@ func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.Marsha
return newSig.Serialize(), nil
}
// SignByCredential signs a node public key by a private key which has its
// signing authority delegated by a SigCredential signature. This is used by
// wrapped auth keys.
func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.NodePublic) (tkatype.MarshaledSignature, error) {
if wrapped.SigKind != SigCredential {
return nil, fmt.Errorf("wrapped signature must be a credential, got %v", wrapped.SigKind)
}
nk, err := nodeKey.MarshalBinary()
if err != nil {
return nil, fmt.Errorf("marshalling node-key: %w", err)
}
sig := &NodeKeySignature{
SigKind: SigRotation,
Pubkey: nk,
Nested: wrapped,
}
sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(privKey, sigHash[:])
return sig.Serialize(), nil
}
// DecodeWrappedAuthkey separates wrapping information from an authkey, if any.
// In all cases the authkey is returned, sans wrapping information if any.
//
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
// and private key.
func DecodeWrappedAuthkey(wrappedAuthKey string, logf logger.Logf) (authKey string, isWrapped bool, sig *NodeKeySignature, priv ed25519.PrivateKey) {
authKey, suffix, found := strings.Cut(wrappedAuthKey, "--TL")
if !found {
return wrappedAuthKey, false, nil, nil
}
sigBytes, privBytes, found := strings.Cut(suffix, "-")
if !found {
// TODO: propagate these errors to `tailscale up` output?
logf("decoding wrapped auth-key: did not find delimiter")
return wrappedAuthKey, false, nil, nil
}
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
if err != nil {
logf("decoding wrapped auth-key: signature decode: %v", err)
return wrappedAuthKey, false, nil, nil
}
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
if err != nil {
logf("decoding wrapped auth-key: priv decode: %v", err)
return wrappedAuthKey, false, nil, nil
}
sig = new(NodeKeySignature)
if err := sig.Unserialize(rawSig); err != nil {
logf("decoding wrapped auth-key: signature: %v", err)
return wrappedAuthKey, false, nil, nil
}
priv = ed25519.PrivateKey(rawPriv)
return authKey, true, sig, priv
}

View File

@@ -356,7 +356,11 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
return sig
},
want: &RotationDetails{
WrappingPubkey: cPub,
InitialSig: &NodeKeySignature{
SigKind: SigCredential,
KeyID: pub,
WrappingPubkey: cPub,
},
},
},
{
@@ -382,8 +386,13 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
return sig
},
want: &RotationDetails{
WrappingPubkey: cPub,
PrevNodeKeys: []key.NodePublic{n1.Public()},
InitialSig: &NodeKeySignature{
SigKind: SigDirect,
Pubkey: n1pub,
KeyID: pub,
WrappingPubkey: cPub,
},
PrevNodeKeys: []key.NodePublic{n1.Public()},
},
},
{
@@ -418,13 +427,23 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
return sig
},
want: &RotationDetails{
WrappingPubkey: cPub,
PrevNodeKeys: []key.NodePublic{n2.Public(), n1.Public()},
InitialSig: &NodeKeySignature{
SigKind: SigDirect,
Pubkey: n1pub,
KeyID: pub,
WrappingPubkey: cPub,
},
PrevNodeKeys: []key.NodePublic{n2.Public(), n1.Public()},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.want != nil {
initialHash := tt.want.InitialSig.SigHash()
tt.want.InitialSig.Signature = ed25519.Sign(priv, initialHash[:])
}
sig := tt.sigFn()
if err := sig.verifySignature(tt.nodeKey, k); err != nil {
t.Fatalf("verifySignature(node) failed: %v", err)
@@ -439,3 +458,42 @@ func TestNodeKeySignatureRotationDetails(t *testing.T) {
})
}
}
func TestDecodeWrappedAuthkey(t *testing.T) {
k, isWrapped, sig, priv := DecodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
}
if isWrapped {
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
}
if sig != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
}
if priv != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
}
k, isWrapped, sig, priv = DecodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
}
if !isWrapped {
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
}
if sig == nil {
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
}
sigHash := sig.SigHash()
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
t.Error("signature failed to verify")
}
// Make sure the private is correct by using it.
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
t.Error("failed to use priv")
}
}

View File

@@ -831,9 +831,18 @@ func (ns *Impl) inject() {
// Only send to the host if this 4via6 route is
// something this node handles.
if ns.lb != nil && ns.lb.ShouldHandleViaIP(srcIP) {
sendToHost = true
dstIP := netip.AddrFrom16(v.DestinationAddress().As16())
// Also, only forward to the host if
// the packet is destined for a local
// IP; otherwise, we'd send traffic
// that's intended for another peer
// from the local 4via6 address to the
// host instead of outbound to
// WireGuard. See:
// https://github.com/tailscale/tailscale/issues/12448
sendToHost = ns.isLocalIP(dstIP)
if debugNetstack() {
ns.logf("netstack: sending 4via6 packet to host: %v", srcIP)
ns.logf("netstack: sending 4via6 packet to host: src=%v dst=%v", srcIP, dstIP)
}
}
}