Compare commits
15 Commits
bradfitz/a
...
v1.38.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47ebe6f956 | ||
|
|
c750186830 | ||
|
|
d7bbd4fe03 | ||
|
|
ac0c0b081d | ||
|
|
068ed7dbfa | ||
|
|
26bf7c4dbe | ||
|
|
d47b74e461 | ||
|
|
3db61d07ca | ||
|
|
817aa282c2 | ||
|
|
d00c046b72 | ||
|
|
aad01c81b1 | ||
|
|
fd558e2e68 | ||
|
|
3eeff9e7f7 | ||
|
|
6c0e6a5f4e | ||
|
|
10d462d321 |
@@ -1 +1 @@
|
||||
1.37.0
|
||||
1.38.3
|
||||
|
||||
@@ -29,7 +29,7 @@ var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
|
||||
func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "funnel",
|
||||
ShortHelp: "[ALPHA] turn Tailscale Funnel on or off",
|
||||
ShortHelp: "[BETA] turn Tailscale Funnel on or off",
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
funnel <serve-port> {on|off}
|
||||
funnel status [--json]
|
||||
|
||||
@@ -40,9 +40,17 @@ var netlockCmd = &ffcli.Command{
|
||||
nlDisablementKDFCmd,
|
||||
nlLogCmd,
|
||||
nlLocalDisableCmd,
|
||||
nlTskeyWrapCmd,
|
||||
},
|
||||
Exec: runNetworkLockStatus,
|
||||
Exec: runNetworkLockNoSubcommand,
|
||||
}
|
||||
|
||||
func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
|
||||
// Detect & handle the deprecated command 'lock tskey-wrap'.
|
||||
if len(args) >= 2 && args[0] == "tskey-wrap" {
|
||||
return runTskeyWrapCmd(ctx, args[1:])
|
||||
}
|
||||
|
||||
return runNetworkLockStatus(ctx, args)
|
||||
}
|
||||
|
||||
var nlInitArgs struct {
|
||||
@@ -427,13 +435,19 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
||||
|
||||
var nlSignCmd = &ffcli.Command{
|
||||
Name: "sign",
|
||||
ShortUsage: "sign <node-key> [<rotation-key>]",
|
||||
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
LongHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
Exec: runNetworkLockSign,
|
||||
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
|
||||
ShortHelp: "Signs a node or pre-approved auth key",
|
||||
LongHelp: `Either:
|
||||
- signs a node key and transmits the signature to the coordination server, or
|
||||
- signs a pre-approved auth key, printing it in a form that can be used to bring up nodes under tailnet lock`,
|
||||
Exec: runNetworkLockSign,
|
||||
}
|
||||
|
||||
func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 && strings.HasPrefix(args[0], "tskey-auth-") {
|
||||
return runTskeyWrapCmd(ctx, args)
|
||||
}
|
||||
|
||||
var (
|
||||
nodeKey key.NodePublic
|
||||
rotationKey key.NLPublic
|
||||
@@ -636,14 +650,6 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlTskeyWrapCmd = &ffcli.Command{
|
||||
Name: "tskey-wrap",
|
||||
ShortUsage: "tskey-wrap <tailscale pre-auth key>",
|
||||
ShortHelp: "Modifies a pre-auth key from the admin panel to work with tailnet lock",
|
||||
LongHelp: "Modifies a pre-auth key from the admin panel to work with tailnet lock",
|
||||
Exec: runTskeyWrapCmd,
|
||||
}
|
||||
|
||||
func runTskeyWrapCmd(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: lock tskey-wrap <tailscale pre-auth key>")
|
||||
@@ -657,21 +663,25 @@ func runTskeyWrapCmd(ctx context.Context, args []string) error {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
|
||||
return wrapAuthKey(ctx, args[0], st)
|
||||
}
|
||||
|
||||
func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) error {
|
||||
// Generate a separate tailnet-lock key just for the credential signature.
|
||||
// We use the free-form meta strings to mark a little bit of metadata about this
|
||||
// key.
|
||||
priv := key.NewNLPrivate()
|
||||
m := map[string]string{
|
||||
"purpose": "pre-auth key",
|
||||
"wrapper_stableid": string(st.Self.ID),
|
||||
"wrapper_stableid": string(status.Self.ID),
|
||||
"wrapper_createtime": fmt.Sprint(time.Now().Unix()),
|
||||
}
|
||||
if strings.HasPrefix(args[0], "tskey-auth-") && strings.Index(args[0][len("tskey-auth-"):], "-") > 0 {
|
||||
if strings.HasPrefix(keyStr, "tskey-auth-") && strings.Index(keyStr[len("tskey-auth-"):], "-") > 0 {
|
||||
// We don't want to accidentally embed the nonce part of the authkey in
|
||||
// the event the format changes. As such, we make sure its in the format we
|
||||
// expect (tskey-auth-<stableID, inc CNTRL suffix>-nonce) before we parse
|
||||
// out and embed the stableID.
|
||||
s := strings.TrimPrefix(args[0], "tskey-auth-")
|
||||
s := strings.TrimPrefix(keyStr, "tskey-auth-")
|
||||
m["authkey_stableid"] = s[:strings.Index(s, "-")]
|
||||
}
|
||||
k := tka.Key{
|
||||
@@ -681,7 +691,7 @@ func runTskeyWrapCmd(ctx context.Context, args []string) error {
|
||||
Meta: m,
|
||||
}
|
||||
|
||||
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, args[0], priv)
|
||||
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, keyStr, priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wrapping failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
|
||||
func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "serve",
|
||||
ShortHelp: "[ALPHA] Serve from your Tailscale node",
|
||||
ShortHelp: "[BETA] Serve from your Tailscale node",
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
serve https:<port> <mount-point> <source> [off]
|
||||
serve tcp:<port> tcp://localhost:<local-port> [off]
|
||||
@@ -41,7 +41,7 @@ serve https:<port> <mount-point> <source> [off]
|
||||
serve status [--json]
|
||||
`),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
*** ALPHA; all of this is subject to change ***
|
||||
*** BETA; all of this is subject to change ***
|
||||
|
||||
The 'tailscale serve' set of commands allows you to serve
|
||||
content and local servers from your Tailscale node to
|
||||
|
||||
@@ -212,7 +212,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
|
||||
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
|
||||
|
||||
@@ -31,10 +31,13 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -82,11 +85,6 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
logf := logger.WithPrefix(b.logf, fmt.Sprintf("cert(%q): ", domain))
|
||||
dir, err := b.certDir()
|
||||
if err != nil {
|
||||
logf("failed to get certDir: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
traceACME := func(v any) {
|
||||
if !acmeDebug() {
|
||||
@@ -96,17 +94,22 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
log.Printf("acme %T: %s", v, j)
|
||||
}
|
||||
|
||||
if pair, err := b.getCertPEMCached(dir, domain, now); err == nil {
|
||||
cs, err := b.getCertStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
future := now.AddDate(0, 0, 14)
|
||||
if b.shouldStartDomainRenewal(dir, domain, future) {
|
||||
if b.shouldStartDomainRenewal(cs, domain, future) {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go b.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, future)
|
||||
}
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, logf, traceACME, dir, domain, now)
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
return nil, err
|
||||
@@ -114,7 +117,7 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, future time.Time) bool {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
now := time.Now()
|
||||
@@ -124,7 +127,7 @@ func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.
|
||||
return false
|
||||
}
|
||||
lastRenewCheck[domain] = now
|
||||
_, err := b.getCertPEMCached(dir, domain, future)
|
||||
_, err := getCertPEMCached(cs, domain, future)
|
||||
return errors.Is(err, errCertExpired)
|
||||
}
|
||||
|
||||
@@ -140,15 +143,32 @@ type certStore interface {
|
||||
WriteCert(domain string, cert []byte) error
|
||||
// WriteKey writes the key for domain.
|
||||
WriteKey(domain string, key []byte) error
|
||||
// ACMEKey returns the value previously stored via WriteACMEKey.
|
||||
// It is a PEM encoded ECDSA key.
|
||||
ACMEKey() ([]byte, error)
|
||||
// WriteACMEKey stores the provided PEM encoded ECDSA key.
|
||||
WriteACMEKey([]byte) error
|
||||
}
|
||||
|
||||
var errCertExpired = errors.New("cert expired")
|
||||
|
||||
func (b *LocalBackend) getCertStore(dir string) certStore {
|
||||
if hostinfo.GetEnvType() == hostinfo.Kubernetes && dir == "/tmp" {
|
||||
return certStateStore{StateStore: b.store}
|
||||
func (b *LocalBackend) getCertStore() (certStore, error) {
|
||||
switch b.store.(type) {
|
||||
case *store.FileStore:
|
||||
case *mem.Store:
|
||||
default:
|
||||
if hostinfo.GetEnvType() == hostinfo.Kubernetes {
|
||||
// We're running in Kubernetes with a custom StateStore,
|
||||
// use that instead of the cert directory.
|
||||
// TODO(maisem): expand this to other environments?
|
||||
return certStateStore{StateStore: b.store}, nil
|
||||
}
|
||||
}
|
||||
return certFileStore{dir: dir}
|
||||
dir, err := b.certDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return certFileStore{dir: dir}, nil
|
||||
}
|
||||
|
||||
// certFileStore implements certStore by storing the cert & key files in the named directory.
|
||||
@@ -160,6 +180,25 @@ type certFileStore struct {
|
||||
testRoots *x509.CertPool
|
||||
}
|
||||
|
||||
const acmePEMName = "acme-account.key.pem"
|
||||
|
||||
func (f certFileStore) ACMEKey() ([]byte, error) {
|
||||
pemName := filepath.Join(f.dir, acmePEMName)
|
||||
v, err := os.ReadFile(pemName)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteACMEKey(b []byte) error {
|
||||
pemName := filepath.Join(f.dir, acmePEMName)
|
||||
return atomicfile.WriteFile(pemName, b, 0600)
|
||||
}
|
||||
|
||||
func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
certPEM, err := os.ReadFile(certFile(f.dir, domain))
|
||||
if err != nil {
|
||||
@@ -182,11 +221,11 @@ func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, erro
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteCert(domain string, cert []byte) error {
|
||||
return os.WriteFile(certFile(f.dir, domain), cert, 0644)
|
||||
return atomicfile.WriteFile(certFile(f.dir, domain), cert, 0644)
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteKey(domain string, key []byte) error {
|
||||
return os.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||
}
|
||||
|
||||
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
|
||||
@@ -221,6 +260,14 @@ func (s certStateStore) WriteKey(domain string, key []byte) error {
|
||||
return s.WriteState(ipn.StateKey(domain+".key"), key)
|
||||
}
|
||||
|
||||
func (s certStateStore) ACMEKey() ([]byte, error) {
|
||||
return s.ReadState(ipn.StateKey(acmePEMName))
|
||||
}
|
||||
|
||||
func (s certStateStore) WriteACMEKey(key []byte) error {
|
||||
return s.WriteState(ipn.StateKey(acmePEMName), key)
|
||||
}
|
||||
|
||||
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
|
||||
// from cache or freshly obtained.
|
||||
type TLSCertKeyPair struct {
|
||||
@@ -236,26 +283,26 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
|
||||
// domain exists on disk in dir that is valid at the provided now time.
|
||||
// If the keypair is expired, it returns errCertExpired.
|
||||
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
|
||||
func (b *LocalBackend) getCertPEMCached(dir, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
// Before we read files from disk using it, validate it's halfway
|
||||
// reasonable looking.
|
||||
return nil, fmt.Errorf("invalid domain %q", domain)
|
||||
}
|
||||
return b.getCertStore(dir).Read(domain, now)
|
||||
return cs.Read(domain, now)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(any), dir, domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
if p, err := b.getCertPEMCached(dir, domain, now); err == nil {
|
||||
if p, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
return p, nil
|
||||
} else if !errors.Is(err, ipn.ErrStateNotExist) && !errors.Is(err, errCertExpired) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := acmeKey(dir)
|
||||
key, err := acmeKey(cs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acmeKey: %w", err)
|
||||
}
|
||||
@@ -366,8 +413,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceAC
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certStore := b.getCertStore(dir)
|
||||
if err := certStore.WriteKey(domain, privPEM.Bytes()); err != nil {
|
||||
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -390,7 +436,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceAC
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := certStore.WriteCert(domain, certPEM.Bytes()); err != nil {
|
||||
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -444,14 +490,15 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||
}
|
||||
|
||||
func acmeKey(dir string) (crypto.Signer, error) {
|
||||
pemName := filepath.Join(dir, "acme-account.key.pem")
|
||||
if v, err := os.ReadFile(pemName); err == nil {
|
||||
func acmeKey(cs certStore) (crypto.Signer, error) {
|
||||
if v, err := cs.ACMEKey(); err == nil {
|
||||
priv, _ := pem.Decode(v)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: invalid account key found in cache")
|
||||
}
|
||||
return parsePrivateKey(priv.Bytes)
|
||||
} else if err != nil && !errors.Is(err, ipn.ErrStateNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
@@ -462,7 +509,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
|
||||
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
|
||||
if err := cs.WriteACMEKey(pemBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
|
||||
@@ -298,7 +298,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
e: e,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
store: store,
|
||||
dialer: dialer,
|
||||
backendLogID: logid,
|
||||
state: ipn.NoState,
|
||||
|
||||
@@ -439,18 +439,26 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||
}
|
||||
rp := httputil.NewSingleHostReverseProxy(u)
|
||||
rp.Transport = &http.Transport{
|
||||
DialContext: b.dialer.SystemDial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
rp := &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(u)
|
||||
r.Out.Host = r.In.Host
|
||||
if c, ok := r.Out.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext); ok {
|
||||
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
|
||||
}
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
DialContext: b.dialer.SystemDial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
},
|
||||
// Values for the following parameters have been copied from http.DefaultTransport.
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
// Values for the following parameters have been copied from http.DefaultTransport.
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
return rp, nil
|
||||
}
|
||||
@@ -476,7 +484,12 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "unknown proxy destination", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
p.(http.Handler).ServeHTTP(w, r)
|
||||
h := p.(http.Handler)
|
||||
// Trim the mount point from the URL path before proxying. (#6571)
|
||||
if r.URL.Path != "/" {
|
||||
h = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), h)
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
|
||||
// and port.
|
||||
// It checks:
|
||||
// 1. an invite was used to join the Funnel alpha
|
||||
// 1. Funnel is enabled on the Tailnet
|
||||
// 2. HTTPS is enabled on the Tailnet
|
||||
// 3. the node has the "funnel" nodeAttr
|
||||
// 4. the port is allowed for Funnel
|
||||
@@ -190,7 +190,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
// Funnel.
|
||||
func CheckFunnelAccess(port uint16, nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/s/no-funnel.")
|
||||
return errors.New("Funnel not enabled; See https://tailscale.com/s/no-funnel.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
|
||||
|
||||
@@ -153,11 +153,9 @@ func LocalAddresses() (regular, loopback []netip.Addr, err error) {
|
||||
if len(regular4) == 0 && len(regular6) == 0 {
|
||||
// if we have no usable IP addresses then be willing to accept
|
||||
// addresses we otherwise wouldn't, like:
|
||||
// + 169.254.x.x (AWS Lambda uses NAT with these)
|
||||
// + 169.254.x.x (AWS Lambda and Azure App Services use NAT with these)
|
||||
// + IPv6 ULA (Google Cloud Run uses these with address translation)
|
||||
if hostinfo.GetEnvType() == hostinfo.AWSLambda {
|
||||
regular4 = linklocal4
|
||||
}
|
||||
regular4 = linklocal4
|
||||
regular6 = ula6
|
||||
}
|
||||
regular = append(regular4, regular6...)
|
||||
@@ -645,7 +643,14 @@ func isUsableV4(ip netip.Addr) bool {
|
||||
return false
|
||||
}
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
return hostinfo.GetEnvType() == hostinfo.AWSLambda
|
||||
switch hostinfo.GetEnvType() {
|
||||
case hostinfo.AWSLambda:
|
||||
return true
|
||||
case hostinfo.AzureAppService:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ type sockStatCounters struct {
|
||||
txBytes, rxBytes atomic.Uint64
|
||||
rxBytesByInterface, txBytesByInterface map[int]*atomic.Uint64
|
||||
|
||||
txBytesMetric, rxBytesMetric *clientmetric.Metric
|
||||
txBytesMetric, rxBytesMetric, txBytesCellularMetric, rxBytesCellularMetric *clientmetric.Metric
|
||||
|
||||
// Validate counts for TCP sockets by using the TCP_CONNECTION_INFO
|
||||
// getsockopt. We get current counts, as well as save final values when
|
||||
@@ -65,10 +65,12 @@ func withSockStats(ctx context.Context, label Label) context.Context {
|
||||
counters, ok := sockStats.countersByLabel[label]
|
||||
if !ok {
|
||||
counters = &sockStatCounters{
|
||||
rxBytesByInterface: make(map[int]*atomic.Uint64),
|
||||
txBytesByInterface: make(map[int]*atomic.Uint64),
|
||||
txBytesMetric: clientmetric.NewCounter(fmt.Sprintf("sockstats_tx_bytes_%s", label)),
|
||||
rxBytesMetric: clientmetric.NewCounter(fmt.Sprintf("sockstats_rx_bytes_%s", label)),
|
||||
rxBytesByInterface: make(map[int]*atomic.Uint64),
|
||||
txBytesByInterface: make(map[int]*atomic.Uint64),
|
||||
txBytesMetric: clientmetric.NewCounter(fmt.Sprintf("sockstats_tx_bytes_%s", label)),
|
||||
rxBytesMetric: clientmetric.NewCounter(fmt.Sprintf("sockstats_rx_bytes_%s", label)),
|
||||
txBytesCellularMetric: clientmetric.NewCounter(fmt.Sprintf("sockstats_tx_bytes_cellular_%s", label)),
|
||||
rxBytesCellularMetric: clientmetric.NewCounter(fmt.Sprintf("sockstats_rx_bytes_cellular_%s", label)),
|
||||
}
|
||||
|
||||
// We might be called before setLinkMonitor has been called (and we've
|
||||
@@ -119,6 +121,7 @@ func withSockStats(ctx context.Context, label Label) context.Context {
|
||||
}
|
||||
if sockStats.currentInterfaceCellular.Load() {
|
||||
sockStats.rxBytesCellularMetric.Add(int64(n))
|
||||
counters.rxBytesCellularMetric.Add(int64(n))
|
||||
}
|
||||
}
|
||||
didWrite := func(n int) {
|
||||
@@ -132,6 +135,7 @@ func withSockStats(ctx context.Context, label Label) context.Context {
|
||||
}
|
||||
if sockStats.currentInterfaceCellular.Load() {
|
||||
sockStats.txBytesCellularMetric.Add(int64(n))
|
||||
counters.txBytesCellularMetric.Add(int64(n))
|
||||
}
|
||||
}
|
||||
willOverwrite := func(trace *net.SockTrace) {
|
||||
|
||||
@@ -235,6 +235,7 @@ func beIncubator(args []string) error {
|
||||
if err == nil && sessionCloser != nil {
|
||||
defer sessionCloser()
|
||||
}
|
||||
|
||||
var groupIDs []int
|
||||
for _, g := range strings.Split(ia.groups, ",") {
|
||||
gid, err := strconv.ParseInt(g, 10, 32)
|
||||
@@ -244,22 +245,10 @@ func beIncubator(args []string) error {
|
||||
groupIDs = append(groupIDs, int(gid))
|
||||
}
|
||||
|
||||
if err := setGroups(groupIDs); err != nil {
|
||||
if err := dropPrivileges(logf, int(ia.uid), ia.gid, groupIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if egid := os.Getegid(); egid != ia.gid {
|
||||
if err := syscall.Setgid(int(ia.gid)); err != nil {
|
||||
logf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if euid != ia.uid {
|
||||
// Switch users if required before starting the desired process.
|
||||
if err := syscall.Setuid(int(ia.uid)); err != nil {
|
||||
logf(err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
if ia.isSFTP {
|
||||
logf("handling sftp")
|
||||
|
||||
@@ -304,6 +293,108 @@ func beIncubator(args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(andrew-d): verify that this works in more configurations before
|
||||
// enabling by default.
|
||||
const assertDropPrivileges = false
|
||||
|
||||
// dropPrivileges contains all the logic for dropping privileges to a different
|
||||
// UID, GID, and set of supplementary groups. This function is
|
||||
// security-sensitive and ordering-dependent; please be very cautious if/when
|
||||
// refactoring.
|
||||
//
|
||||
// WARNING: if you change this function, you *MUST* run the TestDropPrivileges
|
||||
// test in this package as root on at least Linux, FreeBSD and Darwin. This can
|
||||
// be done by running:
|
||||
//
|
||||
// go test -c ./ssh/tailssh/ && sudo ./tailssh.test -test.v -test.run TestDropPrivileges
|
||||
func dropPrivileges(logf logger.Logf, wantUid, wantGid int, supplementaryGroups []int) error {
|
||||
fatalf := func(format string, args ...any) {
|
||||
logf(format, args...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
euid := os.Geteuid()
|
||||
egid := os.Getegid()
|
||||
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
|
||||
// On FreeBSD and Darwin, the first entry returned from the
|
||||
// getgroups(2) syscall is the egid, and changing it with
|
||||
// setgroups(2) changes the egid of the process. This is
|
||||
// technically a violation of the POSIX standard; see the
|
||||
// following article for more detail:
|
||||
// https://www.usenix.org/system/files/login/articles/325-tsafrir.pdf
|
||||
//
|
||||
// In this case, we add an entry at the beginning of the
|
||||
// groupIDs list containing the expected gid if it's not
|
||||
// already there, which modifies the egid and additional groups
|
||||
// as one unit.
|
||||
if len(supplementaryGroups) == 0 || supplementaryGroups[0] != wantGid {
|
||||
supplementaryGroups = append([]int{wantGid}, supplementaryGroups...)
|
||||
}
|
||||
}
|
||||
|
||||
if err := setGroups(supplementaryGroups); err != nil {
|
||||
return err
|
||||
}
|
||||
if egid != wantGid {
|
||||
// On FreeBSD and Darwin, we may have already called the
|
||||
// equivalent of setegid(wantGid) via the call to setGroups,
|
||||
// above. However, per the manpage, setgid(getegid()) is an
|
||||
// allowed operation regardless of privilege level.
|
||||
//
|
||||
// FreeBSD:
|
||||
// The setgid() system call is permitted if the specified ID
|
||||
// is equal to the real group ID or the effective group ID
|
||||
// of the process, or if the effective user ID is that of
|
||||
// the super user.
|
||||
//
|
||||
// Darwin:
|
||||
// The setgid() function is permitted if the effective
|
||||
// user ID is that of the super user, or if the specified
|
||||
// group ID is the same as the effective group ID. If
|
||||
// not, but the specified group ID is the same as the real
|
||||
// group ID, setgid() will set the effective group ID to
|
||||
// the real group ID.
|
||||
if err := syscall.Setgid(wantGid); err != nil {
|
||||
fatalf("Setgid(%d): %v", wantGid, err)
|
||||
}
|
||||
}
|
||||
if euid != wantUid {
|
||||
// Switch users if required before starting the desired process.
|
||||
if err := syscall.Setuid(wantUid); err != nil {
|
||||
fatalf("Setuid(%d): %v", wantUid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// If we changed either the UID or GID, defensively assert that we
|
||||
// cannot reset the it back to our original values, and that the
|
||||
// current egid/euid are the expected values after we change
|
||||
// everything; if not, we exit the process.
|
||||
if assertDropPrivileges {
|
||||
if egid != wantGid {
|
||||
if err := syscall.Setegid(egid); err == nil {
|
||||
fatalf("unexpectedly able to set egid back to %d", egid)
|
||||
}
|
||||
}
|
||||
if euid != wantUid {
|
||||
if err := syscall.Seteuid(euid); err == nil {
|
||||
fatalf("unexpectedly able to set euid back to %d", euid)
|
||||
}
|
||||
}
|
||||
|
||||
if got := os.Getegid(); got != wantGid {
|
||||
fatalf("got egid=%d, want %d", got, wantGid)
|
||||
}
|
||||
if got := os.Geteuid(); got != wantUid {
|
||||
fatalf("got euid=%d, want %d", got, wantUid)
|
||||
}
|
||||
|
||||
// TODO(andrew-d): assert that our supplementary groups are correct
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// launchProcess launches an incubator process for the provided session.
|
||||
// It is responsible for configuring the process execution environment.
|
||||
// The caller can wait for the process to exit by calling cmd.Wait().
|
||||
|
||||
295
ssh/tailssh/privs_test.go
Normal file
295
ssh/tailssh/privs_test.go
Normal file
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux || darwin || freebsd || openbsd || netbsd || dragonfly
|
||||
|
||||
package tailssh
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestDropPrivileges(t *testing.T) {
|
||||
type SubprocInput struct {
|
||||
UID int
|
||||
GID int
|
||||
AdditionalGroups []int
|
||||
}
|
||||
type SubprocOutput struct {
|
||||
UID int
|
||||
GID int
|
||||
EUID int
|
||||
EGID int
|
||||
AdditionalGroups []int
|
||||
}
|
||||
|
||||
if v := os.Getenv("TS_TEST_DROP_PRIVILEGES_CHILD"); v != "" {
|
||||
t.Logf("in child process")
|
||||
|
||||
var input SubprocInput
|
||||
if err := json.Unmarshal([]byte(v), &input); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Get a handle to our provided JSON file before dropping privs.
|
||||
f := os.NewFile(3, "out.json")
|
||||
|
||||
// We're in our subprocess; actually drop privileges now.
|
||||
dropPrivileges(t.Logf, input.UID, input.GID, input.AdditionalGroups)
|
||||
|
||||
additional, _ := syscall.Getgroups()
|
||||
|
||||
// Print our IDs
|
||||
json.NewEncoder(f).Encode(SubprocOutput{
|
||||
UID: os.Getuid(),
|
||||
GID: os.Getgid(),
|
||||
EUID: os.Geteuid(),
|
||||
EGID: os.Getegid(),
|
||||
AdditionalGroups: additional,
|
||||
})
|
||||
|
||||
// Close output file to ensure that it's flushed to disk before we exit
|
||||
f.Close()
|
||||
|
||||
// Always exit the process now that we have a different
|
||||
// UID/GID/etc.; we don't want the Go test framework to try and
|
||||
// clean anything up, since it might no longer have access.
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if os.Getuid() != 0 {
|
||||
t.Skip("test only works when run as root")
|
||||
}
|
||||
|
||||
rerunSelf := func(t *testing.T, input SubprocInput) []byte {
|
||||
fpath := filepath.Join(t.TempDir(), "out.json")
|
||||
outf, err := os.Create(fpath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inputb, err := json.Marshal(input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(os.Args[0], "-test.v", "-test.run", "^"+regexp.QuoteMeta(t.Name())+"$")
|
||||
cmd.Env = append(os.Environ(), "TS_TEST_DROP_PRIVILEGES_CHILD="+string(inputb))
|
||||
cmd.ExtraFiles = []*os.File{outf}
|
||||
cmd.Stdout = logger.FuncWriter(logger.WithPrefix(t.Logf, "child: "))
|
||||
cmd.Stderr = logger.FuncWriter(logger.WithPrefix(t.Logf, "child: "))
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outf.Close()
|
||||
|
||||
jj, err := os.ReadFile(fpath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return jj
|
||||
}
|
||||
|
||||
// We want to ensure we're not colliding with existing users; find some
|
||||
// unused UIDs and GIDs for the tests we run.
|
||||
uid1 := findUnusedUID(t)
|
||||
gid1 := findUnusedGID(t)
|
||||
gid2 := findUnusedGID(t, gid1)
|
||||
gid3 := findUnusedGID(t, gid1, gid2)
|
||||
|
||||
// For some tests, we want a UID/GID pair with the same numerical
|
||||
// value; this finds one.
|
||||
uidgid1 := findUnusedUIDGID(t, uid1, gid1, gid2, gid3)
|
||||
|
||||
t.Logf("uid1=%d gid1=%d gid2=%d gid3=%d uidgid1=%d",
|
||||
uid1, gid1, gid2, gid3, uidgid1)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
uid int
|
||||
gid int
|
||||
additionalGroups []int
|
||||
}{
|
||||
{
|
||||
name: "all_different_values",
|
||||
uid: uid1,
|
||||
gid: gid1,
|
||||
additionalGroups: []int{gid2, gid3},
|
||||
},
|
||||
{
|
||||
name: "no_additional_groups",
|
||||
uid: uid1,
|
||||
gid: gid1,
|
||||
additionalGroups: []int{},
|
||||
},
|
||||
// This is a regression test for the following bug, triggered
|
||||
// on Darwin & FreeBSD:
|
||||
// https://github.com/tailscale/tailscale/issues/7616
|
||||
{
|
||||
name: "same_values",
|
||||
uid: uidgid1,
|
||||
gid: uidgid1,
|
||||
additionalGroups: []int{uidgid1},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
subprocOut := rerunSelf(t, SubprocInput{
|
||||
UID: tt.uid,
|
||||
GID: tt.gid,
|
||||
AdditionalGroups: tt.additionalGroups,
|
||||
})
|
||||
|
||||
var out SubprocOutput
|
||||
if err := json.Unmarshal(subprocOut, &out); err != nil {
|
||||
t.Logf("%s", subprocOut)
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("output: %+v", out)
|
||||
|
||||
if out.UID != tt.uid {
|
||||
t.Errorf("got uid %d; want %d", out.UID, tt.uid)
|
||||
}
|
||||
if out.GID != tt.gid {
|
||||
t.Errorf("got gid %d; want %d", out.GID, tt.gid)
|
||||
}
|
||||
if out.EUID != tt.uid {
|
||||
t.Errorf("got euid %d; want %d", out.EUID, tt.uid)
|
||||
}
|
||||
if out.EGID != tt.gid {
|
||||
t.Errorf("got egid %d; want %d", out.EGID, tt.gid)
|
||||
}
|
||||
|
||||
// On FreeBSD and Darwin, the set of additional groups
|
||||
// is prefixed with the egid; handle that case by
|
||||
// modifying our expected set.
|
||||
wantGroups := make(map[int]bool)
|
||||
for _, id := range tt.additionalGroups {
|
||||
wantGroups[id] = true
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
|
||||
wantGroups[tt.gid] = true
|
||||
}
|
||||
|
||||
gotGroups := make(map[int]bool)
|
||||
for _, id := range out.AdditionalGroups {
|
||||
gotGroups[id] = true
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(gotGroups, wantGroups) {
|
||||
t.Errorf("got additional groups %+v; want %+v", gotGroups, wantGroups)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func findUnusedUID(t *testing.T, not ...int) int {
|
||||
for i := 1000; i < 65535; i++ {
|
||||
// Skip UIDs that might be valid
|
||||
if maybeValidUID(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip UIDs that we're avoiding
|
||||
if slices.Contains(not, i) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Not a valid UID, not one we're avoiding... all good!
|
||||
return i
|
||||
}
|
||||
|
||||
t.Fatalf("unable to find an unused UID")
|
||||
return -1
|
||||
}
|
||||
|
||||
func findUnusedGID(t *testing.T, not ...int) int {
|
||||
for i := 1000; i < 65535; i++ {
|
||||
if maybeValidGID(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip GIDs that we're avoiding
|
||||
if slices.Contains(not, i) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Not a valid GID, not one we're avoiding... all good!
|
||||
return i
|
||||
}
|
||||
|
||||
t.Fatalf("unable to find an unused GID")
|
||||
return -1
|
||||
}
|
||||
|
||||
func findUnusedUIDGID(t *testing.T, not ...int) int {
|
||||
for i := 1000; i < 65535; i++ {
|
||||
if maybeValidUID(i) || maybeValidGID(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip IDs that we're avoiding
|
||||
if slices.Contains(not, i) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Not a valid ID, not one we're avoiding... all good!
|
||||
return i
|
||||
}
|
||||
|
||||
t.Fatalf("unable to find an unused UID/GID pair")
|
||||
return -1
|
||||
}
|
||||
|
||||
func maybeValidUID(id int) bool {
|
||||
_, err := user.LookupId(strconv.Itoa(id))
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
var u1 user.UnknownUserIdError
|
||||
if errors.As(err, &u1) {
|
||||
return false
|
||||
}
|
||||
var u2 user.UnknownUserError
|
||||
if errors.As(err, &u2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Some other error; might be valid
|
||||
return true
|
||||
}
|
||||
|
||||
func maybeValidGID(id int) bool {
|
||||
_, err := user.LookupGroupId(strconv.Itoa(id))
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
var u1 user.UnknownGroupIdError
|
||||
if errors.As(err, &u1) {
|
||||
return false
|
||||
}
|
||||
var u2 user.UnknownGroupError
|
||||
if errors.As(err, &u2) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Some other error; might be valid
|
||||
return true
|
||||
}
|
||||
@@ -1822,7 +1822,8 @@ const (
|
||||
|
||||
// Funnel warning capabilities used for reporting errors to the user.
|
||||
|
||||
// CapabilityWarnFunnelNoInvite indicates an invite has not been accepted for the Funnel alpha.
|
||||
// CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet.
|
||||
// NOTE: In transition from Alpha to Beta, this capability is being reused as the enablement.
|
||||
CapabilityWarnFunnelNoInvite = "https://tailscale.com/cap/warn-funnel-no-invite"
|
||||
|
||||
// CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet.
|
||||
|
||||
@@ -150,14 +150,14 @@ func InfoFrom(dir string) (VersionInfo, error) {
|
||||
}
|
||||
|
||||
// Note, this mechanism doesn't correctly support go.mod replacements,
|
||||
// or go workdirs. We only parse out the commit hash from go.mod's
|
||||
// or go workdirs. We only parse out the commit ref from go.mod's
|
||||
// "require" line, nothing else.
|
||||
tailscaleHash, err := tailscaleModuleHash(modBs)
|
||||
tailscaleRef, err := tailscaleModuleRef(modBs)
|
||||
if err != nil {
|
||||
return VersionInfo{}, err
|
||||
}
|
||||
|
||||
v, err := infoFromCache(tailscaleHash, runner)
|
||||
v, err := infoFromCache(tailscaleRef, runner)
|
||||
if err != nil {
|
||||
return VersionInfo{}, err
|
||||
}
|
||||
@@ -171,9 +171,10 @@ func InfoFrom(dir string) (VersionInfo, error) {
|
||||
return mkOutput(v)
|
||||
}
|
||||
|
||||
// tailscaleModuleHash returns the git hash of the 'require tailscale.com' line
|
||||
// in the given go.mod bytes.
|
||||
func tailscaleModuleHash(modBs []byte) (string, error) {
|
||||
// tailscaleModuleRef returns the git ref of the 'require tailscale.com' line
|
||||
// in the given go.mod bytes. The ref is either a short commit hash, or a git
|
||||
// tag.
|
||||
func tailscaleModuleRef(modBs []byte) (string, error) {
|
||||
mod, err := modfile.Parse("go.mod", modBs, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -187,7 +188,8 @@ func tailscaleModuleHash(modBs []byte) (string, error) {
|
||||
if i := strings.LastIndexByte(req.Mod.Version, '-'); i != -1 {
|
||||
return req.Mod.Version[i+1:], nil
|
||||
}
|
||||
return "", fmt.Errorf("couldn't parse git hash from tailscale.com version %q", req.Mod.Version)
|
||||
// If there are no dashes, the version is a tag.
|
||||
return req.Mod.Version, nil
|
||||
}
|
||||
return "", fmt.Errorf("no require tailscale.com line in go.mod")
|
||||
}
|
||||
@@ -310,7 +312,7 @@ type verInfo struct {
|
||||
// sentinel patch number.
|
||||
const unknownPatchVersion = 9999999
|
||||
|
||||
func infoFromCache(shortHash string, runner dirRunner) (verInfo, error) {
|
||||
func infoFromCache(ref string, runner dirRunner) (verInfo, error) {
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return verInfo{}, fmt.Errorf("Getting user cache dir: %w", err)
|
||||
@@ -324,16 +326,16 @@ func infoFromCache(shortHash string, runner dirRunner) (verInfo, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if !r.ok("git", "cat-file", "-e", shortHash) {
|
||||
if !r.ok("git", "cat-file", "-e", ref) {
|
||||
if !r.ok("git", "fetch", "origin") {
|
||||
return verInfo{}, fmt.Errorf("updating OSS repo failed")
|
||||
}
|
||||
}
|
||||
hash, err := r.output("git", "rev-parse", shortHash)
|
||||
hash, err := r.output("git", "rev-parse", ref)
|
||||
if err != nil {
|
||||
return verInfo{}, err
|
||||
}
|
||||
date, err := r.output("git", "log", "-n1", "--format=%ct", shortHash)
|
||||
date, err := r.output("git", "log", "-n1", "--format=%ct", ref)
|
||||
if err != nil {
|
||||
return verInfo{}, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user