Files
tailscale/ipn/localapi/debug_test.go
T
Mike Jensen 94ea82ab55 ipn/localapi: restrict debug-log access to PermitWrite (#21447)
The debug-log endpoint was gated on PermitRead, which any local user connecting to the world-writable tailscaled socket passes. Although forged log entries are always possible, we should limit the ability for local users to influence that flow with respects to a given node.

This change requires PermitWrite for debug-log, matching the trust level of every other mutating debug endpoint. Also add a buildfeatures.HasDebug guard to debug-dial-types for parity with serveDebug.

Fixes tailscale/corp#48143

Change-Id: I0c0044b6b44fe7cbfb6734ac18bca3dc36eaffbf

Signed-off-by: Mike Jensen <mikej@tailscale.com>
2026-09-25 12:29:56 -06:00

118 lines
2.7 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_debug
package localapi
import (
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/tstest"
)
// TestServeDevSetStateStore verifies writing state keys requires a local admin,
// not just PermitWrite; guards against bypassing serve-config's authz check.
func TestServeDevSetStateStore(t *testing.T) {
tstest.Replace(t, &validLocalHostForTesting, true)
profileID := ipn.ProfileID("test-profile")
tests := []struct {
desc string
permitWrite bool
localAdmin bool
wantStatus int
}{
{
desc: "no-permission",
permitWrite: false,
localAdmin: false,
wantStatus: http.StatusForbidden,
},
{
desc: "write-not-admin",
permitWrite: true,
localAdmin: false,
wantStatus: http.StatusUnauthorized,
},
{
desc: "write-admin",
permitWrite: true,
localAdmin: true,
wantStatus: http.StatusOK,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
h := handlerForTest(t, &Handler{
PermitWrite: tt.permitWrite,
Actor: &ipnauth.TestActor{LocalAdmin: tt.localAdmin},
b: newTestLocalBackend(t),
})
s := httptest.NewServer(h)
t.Cleanup(s.Close)
form := url.Values{
"key": {string(ipn.ServeConfigKey(profileID))},
"value": {"{}"},
}
res, err := s.Client().PostForm(s.URL+"/localapi/v0/dev-set-state-store", form)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode != tt.wantStatus {
t.Errorf("res.StatusCode = %d, want %d", res.StatusCode, tt.wantStatus)
}
})
}
}
func TestServeDebugLogGate(t *testing.T) {
t.Parallel()
tests := []struct {
desc string
permitRead bool
permitWrite bool
wantStatus int
}{
{
desc: "read-only-denied",
permitRead: true,
wantStatus: http.StatusForbidden,
},
{
desc: "write-allowed",
permitRead: true,
permitWrite: true,
wantStatus: http.StatusNoContent,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
h := handlerForTest(t, &Handler{
PermitRead: tt.permitRead,
PermitWrite: tt.permitWrite,
b: newTestLocalBackend(t),
})
req := httptest.NewRequest("POST", "http://local-tailscaled.sock/localapi/v0/debug-log",
strings.NewReader(`{"prefix":"test","lines":["line"]}`))
resp := httptest.NewRecorder()
h.serveDebugLog(resp, req)
if resp.Code != tt.wantStatus {
t.Errorf("resp.Code = %d, want %d; body: %s", resp.Code, tt.wantStatus, resp.Body.String())
}
})
}
}