Skip to content

Commit 596cf05

Browse files
committed
Merge tag 'v1.50.1' into sunos-1.50
Release 1.50.1
2 parents 00529f9 + 8749388 commit 596cf05

File tree

10 files changed

+117
-39
lines changed

10 files changed

+117
-39
lines changed

VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.50.0
1+
1.50.1

cmd/containerboot/main.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@
3636
// - TS_SOCKET: the path where the tailscaled LocalAPI socket should
3737
// be created.
3838
// - TS_AUTH_ONCE: if true, only attempt to log in if not already
39-
// logged in. If false (the default, for backwards
40-
// compatibility), forcibly log in every time the
41-
// container starts.
39+
// logged in. If false, forcibly log in every time the container starts.
40+
// The default until 1.50.0 was false, but that was misleading: until
41+
// 1.50, containerboot used `tailscale up` which would ignore an authkey
42+
// argument if there was already a node key. Effectively, this behaved
43+
// as though TS_AUTH_ONCE were always true.
44+
// In 1.50.0 the change was made to use `tailscale login` instead of `up`,
45+
// and login will reauthenticate every time it is given an authkey.
46+
// In 1.50.1 we set the TS_AUTH_ONCE to true, to match the previously
47+
// observed behavior.
4248
// - TS_SERVE_CONFIG: if specified, is the file path where the ipn.ServeConfig is located.
4349
// It will be applied once tailscaled is up and running. If the file contains
4450
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
@@ -103,7 +109,7 @@ func main() {
103109
SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
104110
HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
105111
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
106-
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
112+
AuthOnce: defaultBool("TS_AUTH_ONCE", true),
107113
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
108114
}
109115

@@ -252,10 +258,13 @@ authLoop:
252258
if err := tailscaleSet(ctx, cfg); err != nil {
253259
log.Fatalf("failed to auth tailscale: %v", err)
254260
}
255-
// Remove any serve config that may have been set by a previous
256-
// run of containerboot.
257-
if err := client.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil {
258-
log.Fatalf("failed to unset serve config: %v", err)
261+
262+
if cfg.ServeConfigPath != "" {
263+
// Remove any serve config that may have been set by a previous run of
264+
// containerboot, but only if we're providing a new one.
265+
if err := client.SetServeConfig(ctx, new(ipn.ServeConfig)); err != nil {
266+
log.Fatalf("failed to unset serve config: %v", err)
267+
}
259268
}
260269

261270
if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {

cmd/containerboot/main_test.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,10 @@ func TestContainerBoot(t *testing.T) {
129129
{
130130
// Out of the box default: runs in userspace mode, ephemeral storage, interactive login.
131131
Name: "no_args",
132-
Env: nil,
132+
Env: map[string]string{
133+
"TS_AUTH_ONCE": "false",
134+
},
135+
133136
Phases: []phase{
134137
{
135138
WantCmds: []string{
@@ -149,7 +152,8 @@ func TestContainerBoot(t *testing.T) {
149152
// Userspace mode, ephemeral storage, authkey provided on every run.
150153
Name: "authkey",
151154
Env: map[string]string{
152-
"TS_AUTHKEY": "tskey-key",
155+
"TS_AUTHKEY": "tskey-key",
156+
"TS_AUTH_ONCE": "false",
153157
},
154158
Phases: []phase{
155159
{
@@ -170,7 +174,8 @@ func TestContainerBoot(t *testing.T) {
170174
// Userspace mode, ephemeral storage, authkey provided on every run.
171175
Name: "authkey-old-flag",
172176
Env: map[string]string{
173-
"TS_AUTH_KEY": "tskey-key",
177+
"TS_AUTH_KEY": "tskey-key",
178+
"TS_AUTH_ONCE": "false",
174179
},
175180
Phases: []phase{
176181
{
@@ -192,6 +197,7 @@ func TestContainerBoot(t *testing.T) {
192197
Env: map[string]string{
193198
"TS_AUTHKEY": "tskey-key",
194199
"TS_STATE_DIR": filepath.Join(d, "tmp"),
200+
"TS_AUTH_ONCE": "false",
195201
},
196202
Phases: []phase{
197203
{
@@ -211,8 +217,9 @@ func TestContainerBoot(t *testing.T) {
211217
{
212218
Name: "routes",
213219
Env: map[string]string{
214-
"TS_AUTHKEY": "tskey-key",
215-
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
220+
"TS_AUTHKEY": "tskey-key",
221+
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
222+
"TS_AUTH_ONCE": "false",
216223
},
217224
Phases: []phase{
218225
{
@@ -239,6 +246,7 @@ func TestContainerBoot(t *testing.T) {
239246
"TS_AUTHKEY": "tskey-key",
240247
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
241248
"TS_USERSPACE": "false",
249+
"TS_AUTH_ONCE": "false",
242250
},
243251
Phases: []phase{
244252
{
@@ -265,6 +273,7 @@ func TestContainerBoot(t *testing.T) {
265273
"TS_AUTHKEY": "tskey-key",
266274
"TS_ROUTES": "::/64,1::/64",
267275
"TS_USERSPACE": "false",
276+
"TS_AUTH_ONCE": "false",
268277
},
269278
Phases: []phase{
270279
{
@@ -291,6 +300,7 @@ func TestContainerBoot(t *testing.T) {
291300
"TS_AUTHKEY": "tskey-key",
292301
"TS_ROUTES": "::/64,1.2.3.0/24",
293302
"TS_USERSPACE": "false",
303+
"TS_AUTH_ONCE": "false",
294304
},
295305
Phases: []phase{
296306
{
@@ -317,6 +327,7 @@ func TestContainerBoot(t *testing.T) {
317327
"TS_AUTHKEY": "tskey-key",
318328
"TS_DEST_IP": "1.2.3.4",
319329
"TS_USERSPACE": "false",
330+
"TS_AUTH_ONCE": "false",
320331
},
321332
Phases: []phase{
322333
{
@@ -341,6 +352,7 @@ func TestContainerBoot(t *testing.T) {
341352
"TS_AUTHKEY": "tskey-key",
342353
"TS_TAILNET_TARGET_IP": "100.99.99.99",
343354
"TS_USERSPACE": "false",
355+
"TS_AUTH_ONCE": "false",
344356
},
345357
Phases: []phase{
346358
{
@@ -393,6 +405,7 @@ func TestContainerBoot(t *testing.T) {
393405
Env: map[string]string{
394406
"KUBERNETES_SERVICE_HOST": kube.Host,
395407
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
408+
"TS_AUTH_ONCE": "false",
396409
},
397410
KubeSecret: map[string]string{
398411
"authkey": "tskey-key",
@@ -430,6 +443,7 @@ func TestContainerBoot(t *testing.T) {
430443
"TS_KUBE_SECRET": "",
431444
"TS_STATE_DIR": filepath.Join(d, "tmp"),
432445
"TS_AUTHKEY": "tskey-key",
446+
"TS_AUTH_ONCE": "false",
433447
},
434448
KubeSecret: map[string]string{},
435449
Phases: []phase{
@@ -455,6 +469,7 @@ func TestContainerBoot(t *testing.T) {
455469
"KUBERNETES_SERVICE_HOST": kube.Host,
456470
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
457471
"TS_AUTHKEY": "tskey-key",
472+
"TS_AUTH_ONCE": "false",
458473
},
459474
KubeSecret: map[string]string{},
460475
KubeDenyPatch: true,
@@ -524,6 +539,7 @@ func TestContainerBoot(t *testing.T) {
524539
Env: map[string]string{
525540
"KUBERNETES_SERVICE_HOST": kube.Host,
526541
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
542+
"TS_AUTH_ONCE": "false",
527543
},
528544
KubeSecret: map[string]string{
529545
"authkey": "tskey-key",
@@ -575,6 +591,7 @@ func TestContainerBoot(t *testing.T) {
575591
Env: map[string]string{
576592
"TS_SOCKS5_SERVER": "localhost:1080",
577593
"TS_OUTBOUND_HTTP_PROXY_LISTEN": "localhost:8080",
594+
"TS_AUTH_ONCE": "false",
578595
},
579596
Phases: []phase{
580597
{
@@ -595,6 +612,7 @@ func TestContainerBoot(t *testing.T) {
595612
Name: "dns",
596613
Env: map[string]string{
597614
"TS_ACCEPT_DNS": "true",
615+
"TS_AUTH_ONCE": "false",
598616
},
599617
Phases: []phase{
600618
{
@@ -616,6 +634,7 @@ func TestContainerBoot(t *testing.T) {
616634
Env: map[string]string{
617635
"TS_EXTRA_ARGS": "--widget=rotated",
618636
"TS_TAILSCALED_EXTRA_ARGS": "--experiments=widgets",
637+
"TS_AUTH_ONCE": "false",
619638
},
620639
Phases: []phase{
621640
{
@@ -635,7 +654,8 @@ func TestContainerBoot(t *testing.T) {
635654
{
636655
Name: "hostname",
637656
Env: map[string]string{
638-
"TS_HOSTNAME": "my-server",
657+
"TS_HOSTNAME": "my-server",
658+
"TS_AUTH_ONCE": "false",
639659
},
640660
Phases: []phase{
641661
{

cmd/derper/depaware.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
270270
runtime/metrics from github.com/prometheus/client_golang/prometheus+
271271
runtime/pprof from net/http/pprof
272272
runtime/trace from net/http/pprof
273-
slices from tailscale.com/ipn+
273+
slices from tailscale.com/ipn/ipnstate+
274274
sort from compress/flate+
275275
strconv from compress/flate+
276276
strings from bufio+

cmd/tailscale/cli/funnel.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,12 @@ func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, st *ipnstate.Status,
164164
// the feature flag on.
165165
// TODO(sonia,tailscale/corp#10577): Remove this fallback once the
166166
// control flag is turned on for all domains.
167-
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
167+
if err := ipn.CheckFunnelAccess(port, st.Self); err != nil {
168168
return err
169169
}
170170
default:
171171
// Done with enablement, make sure the requested port is allowed.
172-
if err := ipn.CheckFunnelPort(port, st.Self.Capabilities); err != nil {
172+
if err := ipn.CheckFunnelPort(port, st.Self); err != nil {
173173
return err
174174
}
175175
}

ipn/serve.go

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import (
99
"net"
1010
"net/netip"
1111
"net/url"
12-
"slices"
1312
"strconv"
1413
"strings"
1514

15+
"tailscale.com/ipn/ipnstate"
1616
"tailscale.com/tailcfg"
1717
)
1818

@@ -237,48 +237,72 @@ func (sc *ServeConfig) IsFunnelOn() bool {
237237
// 2. the node has the "funnel" nodeAttr
238238
// 3. the port is allowed for Funnel
239239
//
240-
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
241-
// the attribute we're checking for and possibly warning-capabilities for
242-
// Funnel.
243-
func CheckFunnelAccess(port uint16, nodeAttrs []tailcfg.NodeCapability) error {
244-
if !slices.Contains(nodeAttrs, tailcfg.CapabilityHTTPS) {
240+
// The node arg should be the ipnstate.Status.Self node.
241+
func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error {
242+
if !node.HasCap(tailcfg.CapabilityHTTPS) {
245243
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
246244
}
247-
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
245+
if !node.HasCap(tailcfg.NodeAttrFunnel) {
248246
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.")
249247
}
250-
return CheckFunnelPort(port, nodeAttrs)
248+
return CheckFunnelPort(port, node)
251249
}
252250

253251
// CheckFunnelPort checks whether the given port is allowed for Funnel.
254252
// It uses the tailcfg.CapabilityFunnelPorts nodeAttr to determine the allowed
255253
// ports.
256-
func CheckFunnelPort(wantedPort uint16, nodeAttrs []tailcfg.NodeCapability) error {
254+
func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error {
257255
deny := func(allowedPorts string) error {
258256
if allowedPorts == "" {
259257
return fmt.Errorf("port %d is not allowed for funnel", wantedPort)
260258
}
261259
return fmt.Errorf("port %d is not allowed for funnel; allowed ports are: %v", wantedPort, allowedPorts)
262260
}
263261
var portsStr string
264-
for _, attr := range nodeAttrs {
265-
attr := string(attr)
266-
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
267-
continue
268-
}
262+
parseAttr := func(attr string) (string, error) {
269263
u, err := url.Parse(attr)
270264
if err != nil {
271-
return deny("")
265+
return "", deny("")
272266
}
273-
portsStr = u.Query().Get("ports")
267+
portsStr := u.Query().Get("ports")
274268
if portsStr == "" {
275-
return deny("")
269+
return "", deny("")
276270
}
277271
u.RawQuery = ""
278272
if u.String() != string(tailcfg.CapabilityFunnelPorts) {
279-
return deny("")
273+
return "", deny("")
274+
}
275+
return portsStr, nil
276+
}
277+
for attr := range node.CapMap {
278+
attr := string(attr)
279+
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
280+
continue
281+
}
282+
var err error
283+
portsStr, err = parseAttr(attr)
284+
if err != nil {
285+
return err
286+
}
287+
break
288+
}
289+
if portsStr == "" {
290+
for _, attr := range node.Capabilities {
291+
attr := string(attr)
292+
if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) {
293+
continue
294+
}
295+
var err error
296+
portsStr, err = parseAttr(attr)
297+
if err != nil {
298+
return err
299+
}
300+
break
280301
}
281302
}
303+
if portsStr == "" {
304+
return deny("")
305+
}
282306
wantedPortString := strconv.Itoa(int(wantedPort))
283307
for _, ps := range strings.Split(portsStr, ",") {
284308
if ps == "" {

ipn/serve_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package ipn
55
import (
66
"testing"
77

8+
"tailscale.com/ipn/ipnstate"
89
"tailscale.com/tailcfg"
910
)
1011

@@ -26,7 +27,11 @@ func TestCheckFunnelAccess(t *testing.T) {
2627
{3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true},
2728
}
2829
for _, tt := range tests {
29-
err := CheckFunnelAccess(tt.port, tt.caps)
30+
cm := tailcfg.NodeCapMap{}
31+
for _, c := range tt.caps {
32+
cm[c] = nil
33+
}
34+
err := CheckFunnelAccess(tt.port, &ipnstate.PeerStatus{CapMap: cm})
3035
switch {
3136
case err != nil && tt.wantErr,
3237
err == nil && !tt.wantErr:

net/portmapper/portmapper.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,16 @@ func getUPnPErrorsMetric(code int) *clientmetric.Metric {
10401040
return mm
10411041
}
10421042

1043-
mm = clientmetric.NewCounter(fmt.Sprintf("portmap_upnp_errors_with_code_%d", code))
1043+
// Metric names cannot contain a hyphen, so we handle negative numbers
1044+
// by prefixing the name with a "minus_".
1045+
var codeStr string
1046+
if code < 0 {
1047+
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_minus_%d", -code)
1048+
} else {
1049+
codeStr = fmt.Sprintf("portmap_upnp_errors_with_code_%d", code)
1050+
}
1051+
1052+
mm = clientmetric.NewCounter(codeStr)
10441053
mak.Set(&metricUPnPErrorsByCode, code, mm)
10451054
return mm
10461055
}

net/portmapper/portmapper_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,14 @@ func TestPCPIntegration(t *testing.T) {
124124
t.Errorf("got nil mapping after successful createOrGetMapping")
125125
}
126126
}
127+
128+
// Test to ensure that metric names generated by this function do not contain
129+
// invalid characters.
130+
//
131+
// See https://github.com/tailscale/tailscale/issues/9551
132+
func TestGetUPnPErrorsMetric(t *testing.T) {
133+
// This will panic if the metric name is invalid.
134+
getUPnPErrorsMetric(100)
135+
getUPnPErrorsMetric(0)
136+
getUPnPErrorsMetric(-100)
137+
}

tsnet/tsnet.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ func (s *Server) ListenFunnel(network, addr string, opts ...FunnelOption) (net.L
926926
// flow here instead of CheckFunnelAccess to allow the user to turn on Funnel
927927
// if not already on. Specifically when running from a terminal.
928928
// See cli.serveEnv.verifyFunnelEnabled.
929-
if err := ipn.CheckFunnelAccess(uint16(port), st.Self.Capabilities); err != nil {
929+
if err := ipn.CheckFunnelAccess(uint16(port), st.Self); err != nil {
930930
return nil, err
931931
}
932932

0 commit comments

Comments
 (0)