peerapi_test.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package ipnlocal
  4. import (
  5. "context"
  6. "encoding/json"
  7. "net/http"
  8. "net/http/httptest"
  9. "net/netip"
  10. "slices"
  11. "strings"
  12. "testing"
  13. "go4.org/netipx"
  14. "golang.org/x/net/dns/dnsmessage"
  15. "tailscale.com/appc"
  16. "tailscale.com/appc/appctest"
  17. "tailscale.com/health"
  18. "tailscale.com/ipn"
  19. "tailscale.com/ipn/store/mem"
  20. "tailscale.com/tailcfg"
  21. "tailscale.com/tsd"
  22. "tailscale.com/tstest"
  23. "tailscale.com/types/appctype"
  24. "tailscale.com/types/logger"
  25. "tailscale.com/types/netmap"
  26. "tailscale.com/util/eventbus/eventbustest"
  27. "tailscale.com/util/must"
  28. "tailscale.com/util/usermetric"
  29. "tailscale.com/wgengine"
  30. "tailscale.com/wgengine/filter"
  31. )
  32. type peerAPITestEnv struct {
  33. ph *peerAPIHandler
  34. rr *httptest.ResponseRecorder
  35. logBuf tstest.MemLogger
  36. }
  37. type check func(*testing.T, *peerAPITestEnv)
  38. func checks(vv ...check) []check { return vv }
  39. func httpStatus(wantStatus int) check {
  40. return func(t *testing.T, e *peerAPITestEnv) {
  41. if res := e.rr.Result(); res.StatusCode != wantStatus {
  42. t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus)
  43. }
  44. }
  45. }
  46. func bodyContains(sub string) check {
  47. return func(t *testing.T, e *peerAPITestEnv) {
  48. if body := e.rr.Body.String(); !strings.Contains(body, sub) {
  49. t.Errorf("HTTP response body does not contain %q; got: %s", sub, body)
  50. }
  51. }
  52. }
  53. func bodyNotContains(sub string) check {
  54. return func(t *testing.T, e *peerAPITestEnv) {
  55. if body := e.rr.Body.String(); strings.Contains(body, sub) {
  56. t.Errorf("HTTP response body unexpectedly contains %q; got: %s", sub, body)
  57. }
  58. }
  59. }
  60. func TestHandlePeerAPI(t *testing.T) {
  61. tests := []struct {
  62. name string
  63. isSelf bool // the peer sending the request is owned by us
  64. debugCap bool // self node has debug capability
  65. reqs []*http.Request
  66. checks []check
  67. }{
  68. {
  69. name: "not_peer_api",
  70. isSelf: true,
  71. reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
  72. checks: checks(
  73. httpStatus(200),
  74. bodyContains("This is my Tailscale device."),
  75. bodyContains("You are the owner of this node."),
  76. ),
  77. },
  78. {
  79. name: "not_peer_api_not_owner",
  80. isSelf: false,
  81. reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
  82. checks: checks(
  83. httpStatus(200),
  84. bodyContains("This is my Tailscale device."),
  85. bodyNotContains("You are the owner of this node."),
  86. ),
  87. },
  88. {
  89. name: "goroutines/deny-self-no-cap",
  90. isSelf: true,
  91. debugCap: false,
  92. reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
  93. checks: checks(httpStatus(403)),
  94. },
  95. {
  96. name: "goroutines/deny-nonself",
  97. isSelf: false,
  98. debugCap: true,
  99. reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
  100. checks: checks(httpStatus(403)),
  101. },
  102. {
  103. name: "goroutines/accept-self",
  104. isSelf: true,
  105. debugCap: true,
  106. reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
  107. checks: checks(
  108. httpStatus(200),
  109. bodyContains("ServeHTTP"),
  110. ),
  111. },
  112. {
  113. name: "host-val/bad-ip",
  114. isSelf: true,
  115. debugCap: true,
  116. reqs: []*http.Request{httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil)},
  117. checks: checks(
  118. httpStatus(403),
  119. ),
  120. },
  121. {
  122. name: "host-val/no-port",
  123. isSelf: true,
  124. debugCap: true,
  125. reqs: []*http.Request{httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil)},
  126. checks: checks(
  127. httpStatus(403),
  128. ),
  129. },
  130. {
  131. name: "host-val/peer",
  132. isSelf: true,
  133. debugCap: true,
  134. reqs: []*http.Request{httptest.NewRequest("GET", "http://peer/v0/env", nil)},
  135. checks: checks(
  136. httpStatus(200),
  137. ),
  138. },
  139. }
  140. for _, tt := range tests {
  141. t.Run(tt.name, func(t *testing.T) {
  142. selfNode := &tailcfg.Node{
  143. Addresses: []netip.Prefix{
  144. netip.MustParsePrefix("100.100.100.101/32"),
  145. },
  146. }
  147. if tt.debugCap {
  148. selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil}
  149. }
  150. var e peerAPITestEnv
  151. lb := newTestLocalBackend(t)
  152. lb.logf = e.logBuf.Logf
  153. lb.clock = &tstest.Clock{}
  154. lb.currentNode().SetNetMap(&netmap.NetworkMap{SelfNode: selfNode.View()})
  155. e.ph = &peerAPIHandler{
  156. isSelf: tt.isSelf,
  157. selfNode: selfNode.View(),
  158. peerNode: (&tailcfg.Node{
  159. ComputedName: "some-peer-name",
  160. }).View(),
  161. ps: &peerAPIServer{
  162. b: lb,
  163. },
  164. }
  165. for _, req := range tt.reqs {
  166. e.rr = httptest.NewRecorder()
  167. if req.Host == "example.com" {
  168. req.Host = "100.100.100.101:12345"
  169. }
  170. e.ph.ServeHTTP(e.rr, req)
  171. }
  172. for _, f := range tt.checks {
  173. f(t, &e)
  174. }
  175. })
  176. }
  177. }
  178. func TestPeerAPIReplyToDNSQueries(t *testing.T) {
  179. var h peerAPIHandler
  180. h.isSelf = true
  181. if !h.replyToDNSQueries() {
  182. t.Errorf("for isSelf = false; want true")
  183. }
  184. h.isSelf = false
  185. h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
  186. sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
  187. ht := health.NewTracker(sys.Bus.Get())
  188. pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
  189. reg := new(usermetric.Registry)
  190. eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
  191. sys.Set(pm.Store())
  192. sys.Set(eng)
  193. b := newTestLocalBackendWithSys(t, sys)
  194. b.pm = pm
  195. h.ps = &peerAPIServer{b: b}
  196. if h.ps.b.OfferingExitNode() {
  197. t.Fatal("unexpectedly offering exit node")
  198. }
  199. h.ps.b.pm.SetPrefs((&ipn.Prefs{
  200. AdvertiseRoutes: []netip.Prefix{
  201. netip.MustParsePrefix("0.0.0.0/0"),
  202. netip.MustParsePrefix("::/0"),
  203. },
  204. }).View(), ipn.NetworkProfile{})
  205. if !h.ps.b.OfferingExitNode() {
  206. t.Fatal("unexpectedly not offering exit node")
  207. }
  208. if h.replyToDNSQueries() {
  209. t.Errorf("unexpectedly doing DNS without filter")
  210. }
  211. h.ps.b.setFilter(filter.NewAllowNone(logger.Discard, new(netipx.IPSet)))
  212. if h.replyToDNSQueries() {
  213. t.Errorf("unexpectedly doing DNS without filter")
  214. }
  215. f := filter.NewAllowAllForTest(logger.Discard)
  216. h.ps.b.setFilter(f)
  217. if !h.replyToDNSQueries() {
  218. t.Errorf("unexpectedly deny; wanted to be a DNS server")
  219. }
  220. // Also test IPv6.
  221. h.remoteAddr = netip.MustParseAddrPort("[fe70::1]:12345")
  222. if !h.replyToDNSQueries() {
  223. t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
  224. }
  225. }
  226. func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
  227. for _, shouldStore := range []bool{false, true} {
  228. var h peerAPIHandler
  229. h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
  230. sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
  231. ht := health.NewTracker(sys.Bus.Get())
  232. reg := new(usermetric.Registry)
  233. eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
  234. pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
  235. a := appc.NewAppConnector(appc.Config{
  236. Logf: t.Logf,
  237. EventBus: sys.Bus.Get(),
  238. HasStoredRoutes: shouldStore,
  239. })
  240. t.Cleanup(a.Close)
  241. sys.Set(pm.Store())
  242. sys.Set(eng)
  243. b := newTestLocalBackendWithSys(t, sys)
  244. b.pm = pm
  245. b.appConnector = a // configure as an app connector just to enable the API.
  246. h.ps = &peerAPIServer{b: b}
  247. h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
  248. b.CNAMEResource(
  249. dnsmessage.ResourceHeader{
  250. Name: dnsmessage.MustNewName("www.example.com."),
  251. Type: dnsmessage.TypeCNAME,
  252. Class: dnsmessage.ClassINET,
  253. TTL: 0,
  254. },
  255. dnsmessage.CNAMEResource{
  256. CNAME: dnsmessage.MustNewName("example.com."),
  257. },
  258. )
  259. b.AResource(
  260. dnsmessage.ResourceHeader{
  261. Name: dnsmessage.MustNewName("example.com."),
  262. Type: dnsmessage.TypeA,
  263. Class: dnsmessage.ClassINET,
  264. TTL: 0,
  265. },
  266. dnsmessage.AResource{
  267. A: [4]byte{192, 0, 0, 8},
  268. },
  269. )
  270. }}
  271. f := filter.NewAllowAllForTest(logger.Discard)
  272. h.ps.b.setFilter(f)
  273. if !h.replyToDNSQueries() {
  274. t.Errorf("unexpectedly deny; wanted to be a DNS server")
  275. }
  276. w := httptest.NewRecorder()
  277. h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
  278. if w.Code != http.StatusOK {
  279. t.Errorf("unexpected status code: %v", w.Code)
  280. }
  281. var addrs []string
  282. json.NewDecoder(w.Body).Decode(&addrs)
  283. if len(addrs) == 0 {
  284. t.Fatalf("no addresses returned")
  285. }
  286. for _, addr := range addrs {
  287. netip.MustParseAddr(addr)
  288. }
  289. }
  290. }
  291. func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
  292. for _, shouldStore := range []bool{false, true} {
  293. var h peerAPIHandler
  294. h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
  295. sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
  296. bw := eventbustest.NewWatcher(t, sys.Bus.Get())
  297. rc := &appctest.RouteCollector{}
  298. ht := health.NewTracker(sys.Bus.Get())
  299. pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
  300. reg := new(usermetric.Registry)
  301. eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
  302. a := appc.NewAppConnector(appc.Config{
  303. Logf: t.Logf,
  304. EventBus: sys.Bus.Get(),
  305. RouteAdvertiser: rc,
  306. HasStoredRoutes: shouldStore,
  307. })
  308. t.Cleanup(a.Close)
  309. sys.Set(pm.Store())
  310. sys.Set(eng)
  311. b := newTestLocalBackendWithSys(t, sys)
  312. b.pm = pm
  313. b.appConnector = a
  314. h.ps = &peerAPIServer{b: b}
  315. h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
  316. a.Wait(t.Context())
  317. h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
  318. b.AResource(
  319. dnsmessage.ResourceHeader{
  320. Name: dnsmessage.MustNewName("example.com."),
  321. Type: dnsmessage.TypeA,
  322. Class: dnsmessage.ClassINET,
  323. TTL: 0,
  324. },
  325. dnsmessage.AResource{
  326. A: [4]byte{192, 0, 0, 8},
  327. },
  328. )
  329. }}
  330. f := filter.NewAllowAllForTest(logger.Discard)
  331. h.ps.b.setFilter(f)
  332. if !h.ps.b.OfferingAppConnector() {
  333. t.Fatal("expecting to be offering app connector")
  334. }
  335. if !h.replyToDNSQueries() {
  336. t.Errorf("unexpectedly deny; wanted to be a DNS server")
  337. }
  338. w := httptest.NewRecorder()
  339. h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
  340. if w.Code != http.StatusOK {
  341. t.Errorf("unexpected status code: %v", w.Code)
  342. }
  343. a.Wait(t.Context())
  344. wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
  345. if !slices.Equal(rc.Routes(), wantRoutes) {
  346. t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
  347. }
  348. if err := eventbustest.Expect(bw,
  349. eqUpdate(appctype.RouteUpdate{Advertise: mustPrefix("192.0.0.8/32")}),
  350. ); err != nil {
  351. t.Error(err)
  352. }
  353. }
  354. }
  355. func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
  356. for _, shouldStore := range []bool{false, true} {
  357. ctx := context.Background()
  358. var h peerAPIHandler
  359. h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
  360. sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
  361. bw := eventbustest.NewWatcher(t, sys.Bus.Get())
  362. ht := health.NewTracker(sys.Bus.Get())
  363. reg := new(usermetric.Registry)
  364. rc := &appctest.RouteCollector{}
  365. eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
  366. pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
  367. a := appc.NewAppConnector(appc.Config{
  368. Logf: t.Logf,
  369. EventBus: sys.Bus.Get(),
  370. RouteAdvertiser: rc,
  371. HasStoredRoutes: shouldStore,
  372. })
  373. t.Cleanup(a.Close)
  374. sys.Set(pm.Store())
  375. sys.Set(eng)
  376. b := newTestLocalBackendWithSys(t, sys)
  377. b.pm = pm
  378. b.appConnector = a
  379. h.ps = &peerAPIServer{b: b}
  380. h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
  381. a.Wait(ctx)
  382. h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
  383. b.CNAMEResource(
  384. dnsmessage.ResourceHeader{
  385. Name: dnsmessage.MustNewName("www.example.com."),
  386. Type: dnsmessage.TypeCNAME,
  387. Class: dnsmessage.ClassINET,
  388. TTL: 0,
  389. },
  390. dnsmessage.CNAMEResource{
  391. CNAME: dnsmessage.MustNewName("example.com."),
  392. },
  393. )
  394. b.AResource(
  395. dnsmessage.ResourceHeader{
  396. Name: dnsmessage.MustNewName("example.com."),
  397. Type: dnsmessage.TypeA,
  398. Class: dnsmessage.ClassINET,
  399. TTL: 0,
  400. },
  401. dnsmessage.AResource{
  402. A: [4]byte{192, 0, 0, 8},
  403. },
  404. )
  405. }}
  406. f := filter.NewAllowAllForTest(logger.Discard)
  407. h.ps.b.setFilter(f)
  408. if !h.ps.b.OfferingAppConnector() {
  409. t.Fatal("expecting to be offering app connector")
  410. }
  411. if !h.replyToDNSQueries() {
  412. t.Errorf("unexpectedly deny; wanted to be a DNS server")
  413. }
  414. w := httptest.NewRecorder()
  415. h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
  416. if w.Code != http.StatusOK {
  417. t.Errorf("unexpected status code: %v", w.Code)
  418. }
  419. a.Wait(ctx)
  420. wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
  421. if !slices.Equal(rc.Routes(), wantRoutes) {
  422. t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
  423. }
  424. if err := eventbustest.Expect(bw,
  425. eqUpdate(appctype.RouteUpdate{Advertise: mustPrefix("192.0.0.8/32")}),
  426. ); err != nil {
  427. t.Error(err)
  428. }
  429. }
  430. }
  431. type fakeResolver struct {
  432. build func(*dnsmessage.Builder)
  433. }
  434. func (f *fakeResolver) HandlePeerDNSQuery(ctx context.Context, q []byte, from netip.AddrPort, allowName func(name string) bool) (res []byte, err error) {
  435. b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
  436. b.EnableCompression()
  437. b.StartAnswers()
  438. f.build(&b)
  439. return b.Finish()
  440. }