localapi_test.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package localapi
  4. import (
  5. "bytes"
  6. "context"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "go/ast"
  11. "go/parser"
  12. "go/token"
  13. "io"
  14. "log"
  15. "net/http"
  16. "net/http/httptest"
  17. "net/netip"
  18. "net/url"
  19. "os"
  20. "slices"
  21. "strconv"
  22. "strings"
  23. "testing"
  24. "tailscale.com/client/tailscale/apitype"
  25. "tailscale.com/health"
  26. "tailscale.com/ipn"
  27. "tailscale.com/ipn/ipnauth"
  28. "tailscale.com/ipn/ipnlocal"
  29. "tailscale.com/ipn/ipnstate"
  30. "tailscale.com/ipn/store/mem"
  31. "tailscale.com/tailcfg"
  32. "tailscale.com/tsd"
  33. "tailscale.com/tstest"
  34. "tailscale.com/types/key"
  35. "tailscale.com/types/logger"
  36. "tailscale.com/types/logid"
  37. "tailscale.com/util/eventbus/eventbustest"
  38. "tailscale.com/util/slicesx"
  39. "tailscale.com/wgengine"
  40. )
  41. func handlerForTest(t testing.TB, h *Handler) *Handler {
  42. if h.Actor == nil {
  43. h.Actor = &ipnauth.TestActor{}
  44. }
  45. if h.b == nil {
  46. h.b = &ipnlocal.LocalBackend{}
  47. }
  48. if h.logf == nil {
  49. h.logf = logger.TestLogger(t)
  50. }
  51. return h
  52. }
  53. func TestValidHost(t *testing.T) {
  54. tests := []struct {
  55. host string
  56. valid bool
  57. }{
  58. {"", true},
  59. {apitype.LocalAPIHost, true},
  60. {"localhost:9109", false},
  61. {"127.0.0.1:9110", false},
  62. {"[::1]:9111", false},
  63. {"100.100.100.100:41112", false},
  64. {"10.0.0.1:41112", false},
  65. {"37.16.9.210:41112", false},
  66. }
  67. for _, test := range tests {
  68. t.Run(test.host, func(t *testing.T) {
  69. h := handlerForTest(t, &Handler{})
  70. if got := h.validHost(test.host); got != test.valid {
  71. t.Errorf("validHost(%q)=%v, want %v", test.host, got, test.valid)
  72. }
  73. })
  74. }
  75. }
  76. func TestSetPushDeviceToken(t *testing.T) {
  77. tstest.Replace(t, &validLocalHostForTesting, true)
  78. h := handlerForTest(t, &Handler{
  79. PermitWrite: true,
  80. })
  81. s := httptest.NewServer(h)
  82. defer s.Close()
  83. c := s.Client()
  84. want := "my-test-device-token"
  85. body, err := json.Marshal(apitype.SetPushDeviceTokenRequest{PushDeviceToken: want})
  86. if err != nil {
  87. t.Fatal(err)
  88. }
  89. req, err := http.NewRequest("POST", s.URL+"/localapi/v0/set-push-device-token", bytes.NewReader(body))
  90. if err != nil {
  91. t.Fatal(err)
  92. }
  93. res, err := c.Do(req)
  94. if err != nil {
  95. t.Fatal(err)
  96. }
  97. body, err = io.ReadAll(res.Body)
  98. if err != nil {
  99. t.Fatal(err)
  100. }
  101. if res.StatusCode != 200 {
  102. t.Errorf("res.StatusCode=%d, want 200. body: %s", res.StatusCode, body)
  103. }
  104. if got := h.b.GetPushDeviceToken(); got != want {
  105. t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
  106. }
  107. }
  108. type whoIsBackend struct {
  109. whoIs func(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
  110. whoIsNodeKey func(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
  111. peerCaps map[netip.Addr]tailcfg.PeerCapMap
  112. }
  113. func (b whoIsBackend) WhoIs(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  114. return b.whoIs(proto, ipp)
  115. }
  116. func (b whoIsBackend) WhoIsNodeKey(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  117. return b.whoIsNodeKey(k)
  118. }
  119. func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
  120. return b.peerCaps[ip]
  121. }
  122. // Tests that the WhoIs handler accepts IPs, IP:ports, or nodekeys.
  123. //
  124. // From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
  125. //
  126. // And https://github.com/tailscale/tailscale/issues/12465
  127. func TestWhoIsArgTypes(t *testing.T) {
  128. h := handlerForTest(t, &Handler{
  129. PermitRead: true,
  130. })
  131. match := func() (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  132. return (&tailcfg.Node{
  133. ID: 123,
  134. Addresses: []netip.Prefix{
  135. netip.MustParsePrefix("100.101.102.103/32"),
  136. },
  137. }).View(),
  138. tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
  139. true
  140. }
  141. const keyStr = "nodekey:5c8f86d5fc70d924e55f02446165a5dae8f822994ad26bcf4b08fd841f9bf261"
  142. for _, input := range []string{"100.101.102.103", "127.0.0.1:123", keyStr} {
  143. rec := httptest.NewRecorder()
  144. t.Run(input, func(t *testing.T) {
  145. b := whoIsBackend{
  146. whoIs: func(proto string, ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  147. if !strings.Contains(input, ":") {
  148. want := netip.MustParseAddrPort("100.101.102.103:0")
  149. if ipp != want {
  150. t.Fatalf("backend called with %v; want %v", ipp, want)
  151. }
  152. }
  153. return match()
  154. },
  155. whoIsNodeKey: func(k key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  156. if k.String() != keyStr {
  157. t.Fatalf("backend called with %v; want %v", k, keyStr)
  158. }
  159. return match()
  160. },
  161. peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
  162. netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
  163. "foo": {`"bar"`},
  164. },
  165. },
  166. }
  167. h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
  168. if rec.Code != 200 {
  169. t.Fatalf("response code %d", rec.Code)
  170. }
  171. var res apitype.WhoIsResponse
  172. if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
  173. t.Fatalf("parsing response %#q: %v", rec.Body.Bytes(), err)
  174. }
  175. if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
  176. t.Errorf("res.Node.ID=%v, want %v", got, want)
  177. }
  178. if got, want := res.UserProfile.DisplayName, "foo"; got != want {
  179. t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
  180. }
  181. if got, want := len(res.CapMap), 1; got != want {
  182. t.Errorf("capmap size=%v, want %v", got, want)
  183. }
  184. })
  185. }
  186. }
  187. func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
  188. newHandler := func(connIsLocalAdmin bool) *Handler {
  189. return handlerForTest(t, &Handler{
  190. Actor: &ipnauth.TestActor{LocalAdmin: connIsLocalAdmin},
  191. b: newTestLocalBackend(t),
  192. })
  193. }
  194. tests := []struct {
  195. name string
  196. configIn *ipn.ServeConfig
  197. h *Handler
  198. wantErr bool
  199. }{
  200. {
  201. name: "not-path-handler",
  202. configIn: &ipn.ServeConfig{
  203. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  204. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  205. "/": {Proxy: "http://127.0.0.1:3000"},
  206. }},
  207. },
  208. },
  209. h: newHandler(false),
  210. wantErr: false,
  211. },
  212. {
  213. name: "path-handler-admin",
  214. configIn: &ipn.ServeConfig{
  215. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  216. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  217. "/": {Path: "/tmp"},
  218. }},
  219. },
  220. },
  221. h: newHandler(true),
  222. wantErr: false,
  223. },
  224. {
  225. name: "path-handler-not-admin",
  226. configIn: &ipn.ServeConfig{
  227. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  228. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  229. "/": {Path: "/tmp"},
  230. }},
  231. },
  232. },
  233. h: newHandler(false),
  234. wantErr: true,
  235. },
  236. }
  237. for _, tt := range tests {
  238. for _, goos := range []string{"linux", "windows", "darwin", "illumos", "solaris"} {
  239. t.Run(goos+"-"+tt.name, func(t *testing.T) {
  240. err := authorizeServeConfigForGOOSAndUserContext(goos, tt.configIn, tt.h)
  241. gotErr := err != nil
  242. if gotErr != tt.wantErr {
  243. t.Errorf("authorizeServeConfigForGOOSAndUserContext() got error = %v, want error %v", err, tt.wantErr)
  244. }
  245. })
  246. }
  247. }
  248. t.Run("other-goos", func(t *testing.T) {
  249. configIn := &ipn.ServeConfig{
  250. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  251. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  252. "/": {Path: "/tmp"},
  253. }},
  254. },
  255. }
  256. h := newHandler(false)
  257. err := authorizeServeConfigForGOOSAndUserContext("dos", configIn, h)
  258. if err != nil {
  259. t.Errorf("authorizeServeConfigForGOOSAndUserContext() got error = %v, want nil", err)
  260. }
  261. })
  262. }
  263. // TestServeWatchIPNBus used to test that various WatchIPNBus mask flags
  264. // changed the permissions required to access the endpoint.
  265. // However, since the removal of the NotifyNoPrivateKeys flag requirement
  266. // for read-only users, this test now only verifies that the endpoint
  267. // behaves correctly based on the PermitRead and PermitWrite settings.
  268. func TestServeWatchIPNBus(t *testing.T) {
  269. tstest.Replace(t, &validLocalHostForTesting, true)
  270. tests := []struct {
  271. desc string
  272. permitRead, permitWrite bool
  273. wantStatus int
  274. }{
  275. {
  276. desc: "no-permission",
  277. permitRead: false,
  278. permitWrite: false,
  279. wantStatus: http.StatusForbidden,
  280. },
  281. {
  282. desc: "read-only",
  283. permitRead: true,
  284. permitWrite: false,
  285. wantStatus: http.StatusOK,
  286. },
  287. {
  288. desc: "read-and-write",
  289. permitRead: true,
  290. permitWrite: true,
  291. wantStatus: http.StatusOK,
  292. },
  293. }
  294. for _, tt := range tests {
  295. t.Run(tt.desc, func(t *testing.T) {
  296. h := handlerForTest(t, &Handler{
  297. PermitRead: tt.permitRead,
  298. PermitWrite: tt.permitWrite,
  299. b: newTestLocalBackend(t),
  300. })
  301. s := httptest.NewServer(h)
  302. defer s.Close()
  303. c := s.Client()
  304. ctx, cancel := context.WithCancel(context.Background())
  305. req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/localapi/v0/watch-ipn-bus?mask=%d", s.URL, ipn.NotifyInitialState), nil)
  306. if err != nil {
  307. t.Fatal(err)
  308. }
  309. res, err := c.Do(req)
  310. if err != nil {
  311. t.Fatal(err)
  312. }
  313. defer res.Body.Close()
  314. // Cancel the context so that localapi stops streaming IPN bus
  315. // updates.
  316. cancel()
  317. body, err := io.ReadAll(res.Body)
  318. if err != nil && !errors.Is(err, context.Canceled) {
  319. t.Fatal(err)
  320. }
  321. if res.StatusCode != tt.wantStatus {
  322. t.Errorf("res.StatusCode=%d, want %d. body: %s", res.StatusCode, tt.wantStatus, body)
  323. }
  324. })
  325. }
  326. }
  327. func newTestLocalBackend(t testing.TB) *ipnlocal.LocalBackend {
  328. var logf logger.Logf = logger.Discard
  329. sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
  330. store := new(mem.Store)
  331. sys.Set(store)
  332. eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker.Get(), sys.UserMetricsRegistry(), sys.Bus.Get())
  333. if err != nil {
  334. t.Fatalf("NewFakeUserspaceEngine: %v", err)
  335. }
  336. t.Cleanup(eng.Close)
  337. sys.Set(eng)
  338. lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
  339. if err != nil {
  340. t.Fatalf("NewLocalBackend: %v", err)
  341. }
  342. t.Cleanup(lb.Shutdown)
  343. return lb
  344. }
  345. func TestKeepItSorted(t *testing.T) {
  346. // Parse the localapi.go file into an AST.
  347. fset := token.NewFileSet() // positions are relative to fset
  348. src, err := os.ReadFile("localapi.go")
  349. if err != nil {
  350. log.Fatal(err)
  351. }
  352. f, err := parser.ParseFile(fset, "localapi.go", src, 0)
  353. if err != nil {
  354. log.Fatal(err)
  355. }
  356. getHandler := func() *ast.ValueSpec {
  357. for _, d := range f.Decls {
  358. if g, ok := d.(*ast.GenDecl); ok && g.Tok == token.VAR {
  359. for _, s := range g.Specs {
  360. if vs, ok := s.(*ast.ValueSpec); ok {
  361. if len(vs.Names) == 1 && vs.Names[0].Name == "handler" {
  362. return vs
  363. }
  364. }
  365. }
  366. }
  367. }
  368. return nil
  369. }
  370. keys := func() (ret []string) {
  371. h := getHandler()
  372. if h == nil {
  373. t.Fatal("no handler var found")
  374. }
  375. cl, ok := h.Values[0].(*ast.CompositeLit)
  376. if !ok {
  377. t.Fatalf("handler[0] is %T, want *ast.CompositeLit", h.Values[0])
  378. }
  379. for _, e := range cl.Elts {
  380. kv := e.(*ast.KeyValueExpr)
  381. strLt := kv.Key.(*ast.BasicLit)
  382. if strLt.Kind != token.STRING {
  383. t.Fatalf("got: %T, %q", kv.Key, kv.Key)
  384. }
  385. k, err := strconv.Unquote(strLt.Value)
  386. if err != nil {
  387. t.Fatalf("unquote: %v", err)
  388. }
  389. ret = append(ret, k)
  390. }
  391. return
  392. }
  393. gotKeys := keys()
  394. endSlash, noSlash := slicesx.Partition(keys(), func(s string) bool { return strings.HasSuffix(s, "/") })
  395. if !slices.IsSorted(endSlash) {
  396. t.Errorf("the items ending in a slash aren't sorted")
  397. }
  398. if !slices.IsSorted(noSlash) {
  399. t.Errorf("the items ending in a slash aren't sorted")
  400. }
  401. if !t.Failed() {
  402. want := append(endSlash, noSlash...)
  403. if !slices.Equal(gotKeys, want) {
  404. t.Errorf("items with trailing slashes should precede those without")
  405. }
  406. }
  407. }
  408. func TestServeWithUnhealthyState(t *testing.T) {
  409. tstest.Replace(t, &validLocalHostForTesting, true)
  410. h := &Handler{
  411. PermitRead: true,
  412. PermitWrite: true,
  413. b: newTestLocalBackend(t),
  414. logf: t.Logf,
  415. }
  416. h.b.HealthTracker().SetUnhealthy(ipn.StateStoreHealth, health.Args{health.ArgError: "testing"})
  417. if err := h.b.Start(ipn.Options{}); err != nil {
  418. t.Fatal(err)
  419. }
  420. check500Body := func(wantResp string) func(t *testing.T, code int, resp []byte) {
  421. return func(t *testing.T, code int, resp []byte) {
  422. if code != http.StatusInternalServerError {
  423. t.Errorf("got code: %v, want %v\nresponse: %q", code, http.StatusInternalServerError, resp)
  424. }
  425. if got := strings.TrimSpace(string(resp)); got != wantResp {
  426. t.Errorf("got response: %q, want %q", got, wantResp)
  427. }
  428. }
  429. }
  430. tests := []struct {
  431. desc string
  432. req *http.Request
  433. check func(t *testing.T, code int, resp []byte)
  434. }{
  435. {
  436. desc: "status",
  437. req: httptest.NewRequest("GET", "http://localhost:1234/localapi/v0/status", nil),
  438. check: func(t *testing.T, code int, resp []byte) {
  439. if code != http.StatusOK {
  440. t.Errorf("got code: %v, want %v\nresponse: %q", code, http.StatusOK, resp)
  441. }
  442. var status ipnstate.Status
  443. if err := json.Unmarshal(resp, &status); err != nil {
  444. t.Fatal(err)
  445. }
  446. if status.BackendState != "NoState" {
  447. t.Errorf("got backend state: %q, want %q", status.BackendState, "NoState")
  448. }
  449. },
  450. },
  451. {
  452. desc: "login-interactive",
  453. req: httptest.NewRequest("POST", "http://localhost:1234/localapi/v0/login-interactive", nil),
  454. check: check500Body("cannot log in when state store is unhealthy"),
  455. },
  456. {
  457. desc: "start",
  458. req: httptest.NewRequest("POST", "http://localhost:1234/localapi/v0/start", strings.NewReader("{}")),
  459. check: check500Body("cannot start backend when state store is unhealthy"),
  460. },
  461. {
  462. desc: "new-profile",
  463. req: httptest.NewRequest("PUT", "http://localhost:1234/localapi/v0/profiles/", nil),
  464. check: check500Body("cannot log in when state store is unhealthy"),
  465. },
  466. }
  467. for _, tt := range tests {
  468. t.Run(tt.desc, func(t *testing.T) {
  469. resp := httptest.NewRecorder()
  470. h.ServeHTTP(resp, tt.req)
  471. tt.check(t, resp.Code, resp.Body.Bytes())
  472. })
  473. }
  474. }