localapi_test.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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. "io"
  11. "net/http"
  12. "net/http/httptest"
  13. "net/netip"
  14. "net/url"
  15. "strings"
  16. "testing"
  17. "tailscale.com/client/tailscale/apitype"
  18. "tailscale.com/ipn"
  19. "tailscale.com/ipn/ipnlocal"
  20. "tailscale.com/ipn/store/mem"
  21. "tailscale.com/tailcfg"
  22. "tailscale.com/tsd"
  23. "tailscale.com/tstest"
  24. "tailscale.com/types/logger"
  25. "tailscale.com/types/logid"
  26. "tailscale.com/wgengine"
  27. )
  28. func TestValidHost(t *testing.T) {
  29. tests := []struct {
  30. host string
  31. valid bool
  32. }{
  33. {"", true},
  34. {apitype.LocalAPIHost, true},
  35. {"localhost:9109", false},
  36. {"127.0.0.1:9110", false},
  37. {"[::1]:9111", false},
  38. {"100.100.100.100:41112", false},
  39. {"10.0.0.1:41112", false},
  40. {"37.16.9.210:41112", false},
  41. }
  42. for _, test := range tests {
  43. t.Run(test.host, func(t *testing.T) {
  44. h := &Handler{}
  45. if got := h.validHost(test.host); got != test.valid {
  46. t.Errorf("validHost(%q)=%v, want %v", test.host, got, test.valid)
  47. }
  48. })
  49. }
  50. }
  51. func TestSetPushDeviceToken(t *testing.T) {
  52. tstest.Replace(t, &validLocalHostForTesting, true)
  53. h := &Handler{
  54. PermitWrite: true,
  55. b: &ipnlocal.LocalBackend{},
  56. }
  57. s := httptest.NewServer(h)
  58. defer s.Close()
  59. c := s.Client()
  60. want := "my-test-device-token"
  61. body, err := json.Marshal(apitype.SetPushDeviceTokenRequest{PushDeviceToken: want})
  62. if err != nil {
  63. t.Fatal(err)
  64. }
  65. req, err := http.NewRequest("POST", s.URL+"/localapi/v0/set-push-device-token", bytes.NewReader(body))
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. res, err := c.Do(req)
  70. if err != nil {
  71. t.Fatal(err)
  72. }
  73. body, err = io.ReadAll(res.Body)
  74. if err != nil {
  75. t.Fatal(err)
  76. }
  77. if res.StatusCode != 200 {
  78. t.Errorf("res.StatusCode=%d, want 200. body: %s", res.StatusCode, body)
  79. }
  80. if got := h.b.GetPushDeviceToken(); got != want {
  81. t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
  82. }
  83. }
  84. type whoIsBackend struct {
  85. whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
  86. peerCaps map[netip.Addr]tailcfg.PeerCapMap
  87. }
  88. func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  89. return b.whoIs(ipp)
  90. }
  91. func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
  92. return b.peerCaps[ip]
  93. }
  94. // Tests that the WhoIs handler accepts either IPs or IP:ports.
  95. //
  96. // From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
  97. func TestWhoIsJustIP(t *testing.T) {
  98. h := &Handler{
  99. PermitRead: true,
  100. }
  101. for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
  102. rec := httptest.NewRecorder()
  103. t.Run(input, func(t *testing.T) {
  104. b := whoIsBackend{
  105. whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
  106. if !strings.Contains(input, ":") {
  107. want := netip.MustParseAddrPort("100.101.102.103:0")
  108. if ipp != want {
  109. t.Fatalf("backend called with %v; want %v", ipp, want)
  110. }
  111. }
  112. return (&tailcfg.Node{
  113. ID: 123,
  114. Addresses: []netip.Prefix{
  115. netip.MustParsePrefix("100.101.102.103/32"),
  116. },
  117. }).View(),
  118. tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
  119. true
  120. },
  121. peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
  122. netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
  123. "foo": {`"bar"`},
  124. },
  125. },
  126. }
  127. h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
  128. var res apitype.WhoIsResponse
  129. if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
  130. t.Fatal(err)
  131. }
  132. if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
  133. t.Errorf("res.Node.ID=%v, want %v", got, want)
  134. }
  135. if got, want := res.UserProfile.DisplayName, "foo"; got != want {
  136. t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
  137. }
  138. if got, want := len(res.CapMap), 1; got != want {
  139. t.Errorf("capmap size=%v, want %v", got, want)
  140. }
  141. })
  142. }
  143. }
  144. func TestShouldDenyServeConfigForGOOSAndUserContext(t *testing.T) {
  145. newHandler := func(connIsLocalAdmin bool) *Handler {
  146. return &Handler{testConnIsLocalAdmin: &connIsLocalAdmin}
  147. }
  148. tests := []struct {
  149. name string
  150. configIn *ipn.ServeConfig
  151. h *Handler
  152. wantErr bool
  153. }{
  154. {
  155. name: "not-path-handler",
  156. configIn: &ipn.ServeConfig{
  157. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  158. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  159. "/": {Proxy: "http://127.0.0.1:3000"},
  160. }},
  161. },
  162. },
  163. h: newHandler(false),
  164. wantErr: false,
  165. },
  166. {
  167. name: "path-handler-admin",
  168. configIn: &ipn.ServeConfig{
  169. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  170. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  171. "/": {Path: "/tmp"},
  172. }},
  173. },
  174. },
  175. h: newHandler(true),
  176. wantErr: false,
  177. },
  178. {
  179. name: "path-handler-not-admin",
  180. configIn: &ipn.ServeConfig{
  181. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  182. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  183. "/": {Path: "/tmp"},
  184. }},
  185. },
  186. },
  187. h: newHandler(false),
  188. wantErr: true,
  189. },
  190. }
  191. for _, tt := range tests {
  192. for _, goos := range []string{"linux", "windows", "darwin"} {
  193. t.Run(goos+"-"+tt.name, func(t *testing.T) {
  194. err := authorizeServeConfigForGOOSAndUserContext(goos, tt.configIn, tt.h)
  195. gotErr := err != nil
  196. if gotErr != tt.wantErr {
  197. t.Errorf("authorizeServeConfigForGOOSAndUserContext() got error = %v, want error %v", err, tt.wantErr)
  198. }
  199. })
  200. }
  201. }
  202. t.Run("other-goos", func(t *testing.T) {
  203. configIn := &ipn.ServeConfig{
  204. Web: map[ipn.HostPort]*ipn.WebServerConfig{
  205. "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
  206. "/": {Path: "/tmp"},
  207. }},
  208. },
  209. }
  210. h := newHandler(false)
  211. err := authorizeServeConfigForGOOSAndUserContext("dos", configIn, h)
  212. if err != nil {
  213. t.Errorf("authorizeServeConfigForGOOSAndUserContext() got error = %v, want nil", err)
  214. }
  215. })
  216. }
  217. func TestServeWatchIPNBus(t *testing.T) {
  218. tstest.Replace(t, &validLocalHostForTesting, true)
  219. tests := []struct {
  220. desc string
  221. permitRead, permitWrite bool
  222. mask ipn.NotifyWatchOpt // extra bits in addition to ipn.NotifyInitialState
  223. wantStatus int
  224. }{
  225. {
  226. desc: "no-permission",
  227. permitRead: false,
  228. permitWrite: false,
  229. wantStatus: http.StatusForbidden,
  230. },
  231. {
  232. desc: "read-initial-state",
  233. permitRead: true,
  234. permitWrite: false,
  235. wantStatus: http.StatusForbidden,
  236. },
  237. {
  238. desc: "read-initial-state-no-private-keys",
  239. permitRead: true,
  240. permitWrite: false,
  241. mask: ipn.NotifyNoPrivateKeys,
  242. wantStatus: http.StatusOK,
  243. },
  244. {
  245. desc: "read-initial-state-with-private-keys",
  246. permitRead: true,
  247. permitWrite: true,
  248. wantStatus: http.StatusOK,
  249. },
  250. }
  251. for _, tt := range tests {
  252. t.Run(tt.desc, func(t *testing.T) {
  253. h := &Handler{
  254. PermitRead: tt.permitRead,
  255. PermitWrite: tt.permitWrite,
  256. b: newTestLocalBackend(t),
  257. }
  258. s := httptest.NewServer(h)
  259. defer s.Close()
  260. c := s.Client()
  261. ctx, cancel := context.WithCancel(context.Background())
  262. req, err := http.NewRequestWithContext(ctx, "GET", fmt.Sprintf("%s/localapi/v0/watch-ipn-bus?mask=%d", s.URL, ipn.NotifyInitialState|tt.mask), nil)
  263. if err != nil {
  264. t.Fatal(err)
  265. }
  266. res, err := c.Do(req)
  267. if err != nil {
  268. t.Fatal(err)
  269. }
  270. defer res.Body.Close()
  271. // Cancel the context so that localapi stops streaming IPN bus
  272. // updates.
  273. cancel()
  274. body, err := io.ReadAll(res.Body)
  275. if err != nil && !errors.Is(err, context.Canceled) {
  276. t.Fatal(err)
  277. }
  278. if res.StatusCode != tt.wantStatus {
  279. t.Errorf("res.StatusCode=%d, want %d. body: %s", res.StatusCode, tt.wantStatus, body)
  280. }
  281. })
  282. }
  283. }
  284. func newTestLocalBackend(t testing.TB) *ipnlocal.LocalBackend {
  285. var logf logger.Logf = logger.Discard
  286. sys := new(tsd.System)
  287. store := new(mem.Store)
  288. sys.Set(store)
  289. eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
  290. if err != nil {
  291. t.Fatalf("NewFakeUserspaceEngine: %v", err)
  292. }
  293. t.Cleanup(eng.Close)
  294. sys.Set(eng)
  295. lb, err := ipnlocal.NewLocalBackend(logf, logid.PublicID{}, sys, 0)
  296. if err != nil {
  297. t.Fatalf("NewLocalBackend: %v", err)
  298. }
  299. return lb
  300. }