localapi_test.go 11 KB

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