| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package ipnlocal
- import (
- "context"
- "encoding/json"
- "net/http"
- "net/http/httptest"
- "net/netip"
- "slices"
- "strings"
- "testing"
- "go4.org/netipx"
- "golang.org/x/net/dns/dnsmessage"
- "tailscale.com/appc"
- "tailscale.com/appc/appctest"
- "tailscale.com/health"
- "tailscale.com/ipn"
- "tailscale.com/ipn/store/mem"
- "tailscale.com/tailcfg"
- "tailscale.com/tsd"
- "tailscale.com/tstest"
- "tailscale.com/types/appctype"
- "tailscale.com/types/logger"
- "tailscale.com/types/netmap"
- "tailscale.com/util/eventbus/eventbustest"
- "tailscale.com/util/must"
- "tailscale.com/util/usermetric"
- "tailscale.com/wgengine"
- "tailscale.com/wgengine/filter"
- )
- type peerAPITestEnv struct {
- ph *peerAPIHandler
- rr *httptest.ResponseRecorder
- logBuf tstest.MemLogger
- }
- type check func(*testing.T, *peerAPITestEnv)
- func checks(vv ...check) []check { return vv }
- func httpStatus(wantStatus int) check {
- return func(t *testing.T, e *peerAPITestEnv) {
- if res := e.rr.Result(); res.StatusCode != wantStatus {
- t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus)
- }
- }
- }
- func bodyContains(sub string) check {
- return func(t *testing.T, e *peerAPITestEnv) {
- if body := e.rr.Body.String(); !strings.Contains(body, sub) {
- t.Errorf("HTTP response body does not contain %q; got: %s", sub, body)
- }
- }
- }
- func bodyNotContains(sub string) check {
- return func(t *testing.T, e *peerAPITestEnv) {
- if body := e.rr.Body.String(); strings.Contains(body, sub) {
- t.Errorf("HTTP response body unexpectedly contains %q; got: %s", sub, body)
- }
- }
- }
- func TestHandlePeerAPI(t *testing.T) {
- tests := []struct {
- name string
- isSelf bool // the peer sending the request is owned by us
- debugCap bool // self node has debug capability
- reqs []*http.Request
- checks []check
- }{
- {
- name: "not_peer_api",
- isSelf: true,
- reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
- checks: checks(
- httpStatus(200),
- bodyContains("This is my Tailscale device."),
- bodyContains("You are the owner of this node."),
- ),
- },
- {
- name: "not_peer_api_not_owner",
- isSelf: false,
- reqs: []*http.Request{httptest.NewRequest("GET", "/", nil)},
- checks: checks(
- httpStatus(200),
- bodyContains("This is my Tailscale device."),
- bodyNotContains("You are the owner of this node."),
- ),
- },
- {
- name: "goroutines/deny-self-no-cap",
- isSelf: true,
- debugCap: false,
- reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
- checks: checks(httpStatus(403)),
- },
- {
- name: "goroutines/deny-nonself",
- isSelf: false,
- debugCap: true,
- reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
- checks: checks(httpStatus(403)),
- },
- {
- name: "goroutines/accept-self",
- isSelf: true,
- debugCap: true,
- reqs: []*http.Request{httptest.NewRequest("GET", "/v0/goroutines", nil)},
- checks: checks(
- httpStatus(200),
- bodyContains("ServeHTTP"),
- ),
- },
- {
- name: "host-val/bad-ip",
- isSelf: true,
- debugCap: true,
- reqs: []*http.Request{httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil)},
- checks: checks(
- httpStatus(403),
- ),
- },
- {
- name: "host-val/no-port",
- isSelf: true,
- debugCap: true,
- reqs: []*http.Request{httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil)},
- checks: checks(
- httpStatus(403),
- ),
- },
- {
- name: "host-val/peer",
- isSelf: true,
- debugCap: true,
- reqs: []*http.Request{httptest.NewRequest("GET", "http://peer/v0/env", nil)},
- checks: checks(
- httpStatus(200),
- ),
- },
- }
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- selfNode := &tailcfg.Node{
- Addresses: []netip.Prefix{
- netip.MustParsePrefix("100.100.100.101/32"),
- },
- }
- if tt.debugCap {
- selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil}
- }
- var e peerAPITestEnv
- lb := newTestLocalBackend(t)
- lb.logf = e.logBuf.Logf
- lb.clock = &tstest.Clock{}
- lb.currentNode().SetNetMap(&netmap.NetworkMap{SelfNode: selfNode.View()})
- e.ph = &peerAPIHandler{
- isSelf: tt.isSelf,
- selfNode: selfNode.View(),
- peerNode: (&tailcfg.Node{
- ComputedName: "some-peer-name",
- }).View(),
- ps: &peerAPIServer{
- b: lb,
- },
- }
- for _, req := range tt.reqs {
- e.rr = httptest.NewRecorder()
- if req.Host == "example.com" {
- req.Host = "100.100.100.101:12345"
- }
- e.ph.ServeHTTP(e.rr, req)
- }
- for _, f := range tt.checks {
- f(t, &e)
- }
- })
- }
- }
- func TestPeerAPIReplyToDNSQueries(t *testing.T) {
- var h peerAPIHandler
- h.isSelf = true
- if !h.replyToDNSQueries() {
- t.Errorf("for isSelf = false; want true")
- }
- h.isSelf = false
- h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
- sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
- ht := health.NewTracker(sys.Bus.Get())
- pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
- reg := new(usermetric.Registry)
- eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
- sys.Set(pm.Store())
- sys.Set(eng)
- b := newTestLocalBackendWithSys(t, sys)
- b.pm = pm
- h.ps = &peerAPIServer{b: b}
- if h.ps.b.OfferingExitNode() {
- t.Fatal("unexpectedly offering exit node")
- }
- h.ps.b.pm.SetPrefs((&ipn.Prefs{
- AdvertiseRoutes: []netip.Prefix{
- netip.MustParsePrefix("0.0.0.0/0"),
- netip.MustParsePrefix("::/0"),
- },
- }).View(), ipn.NetworkProfile{})
- if !h.ps.b.OfferingExitNode() {
- t.Fatal("unexpectedly not offering exit node")
- }
- if h.replyToDNSQueries() {
- t.Errorf("unexpectedly doing DNS without filter")
- }
- h.ps.b.setFilter(filter.NewAllowNone(logger.Discard, new(netipx.IPSet)))
- if h.replyToDNSQueries() {
- t.Errorf("unexpectedly doing DNS without filter")
- }
- f := filter.NewAllowAllForTest(logger.Discard)
- h.ps.b.setFilter(f)
- if !h.replyToDNSQueries() {
- t.Errorf("unexpectedly deny; wanted to be a DNS server")
- }
- // Also test IPv6.
- h.remoteAddr = netip.MustParseAddrPort("[fe70::1]:12345")
- if !h.replyToDNSQueries() {
- t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
- }
- }
- func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
- for _, shouldStore := range []bool{false, true} {
- var h peerAPIHandler
- h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
- sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
- ht := health.NewTracker(sys.Bus.Get())
- reg := new(usermetric.Registry)
- eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
- pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
- a := appc.NewAppConnector(appc.Config{
- Logf: t.Logf,
- EventBus: sys.Bus.Get(),
- HasStoredRoutes: shouldStore,
- })
- t.Cleanup(a.Close)
- sys.Set(pm.Store())
- sys.Set(eng)
- b := newTestLocalBackendWithSys(t, sys)
- b.pm = pm
- b.appConnector = a // configure as an app connector just to enable the API.
- h.ps = &peerAPIServer{b: b}
- h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
- b.CNAMEResource(
- dnsmessage.ResourceHeader{
- Name: dnsmessage.MustNewName("www.example.com."),
- Type: dnsmessage.TypeCNAME,
- Class: dnsmessage.ClassINET,
- TTL: 0,
- },
- dnsmessage.CNAMEResource{
- CNAME: dnsmessage.MustNewName("example.com."),
- },
- )
- b.AResource(
- dnsmessage.ResourceHeader{
- Name: dnsmessage.MustNewName("example.com."),
- Type: dnsmessage.TypeA,
- Class: dnsmessage.ClassINET,
- TTL: 0,
- },
- dnsmessage.AResource{
- A: [4]byte{192, 0, 0, 8},
- },
- )
- }}
- f := filter.NewAllowAllForTest(logger.Discard)
- h.ps.b.setFilter(f)
- if !h.replyToDNSQueries() {
- t.Errorf("unexpectedly deny; wanted to be a DNS server")
- }
- w := httptest.NewRecorder()
- h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
- if w.Code != http.StatusOK {
- t.Errorf("unexpected status code: %v", w.Code)
- }
- var addrs []string
- json.NewDecoder(w.Body).Decode(&addrs)
- if len(addrs) == 0 {
- t.Fatalf("no addresses returned")
- }
- for _, addr := range addrs {
- netip.MustParseAddr(addr)
- }
- }
- }
- func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
- for _, shouldStore := range []bool{false, true} {
- var h peerAPIHandler
- h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
- sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
- bw := eventbustest.NewWatcher(t, sys.Bus.Get())
- rc := &appctest.RouteCollector{}
- ht := health.NewTracker(sys.Bus.Get())
- pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
- reg := new(usermetric.Registry)
- eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
- a := appc.NewAppConnector(appc.Config{
- Logf: t.Logf,
- EventBus: sys.Bus.Get(),
- RouteAdvertiser: rc,
- HasStoredRoutes: shouldStore,
- })
- t.Cleanup(a.Close)
- sys.Set(pm.Store())
- sys.Set(eng)
- b := newTestLocalBackendWithSys(t, sys)
- b.pm = pm
- b.appConnector = a
- h.ps = &peerAPIServer{b: b}
- h.ps.b.appConnector.UpdateDomains([]string{"example.com"})
- a.Wait(t.Context())
- h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
- b.AResource(
- dnsmessage.ResourceHeader{
- Name: dnsmessage.MustNewName("example.com."),
- Type: dnsmessage.TypeA,
- Class: dnsmessage.ClassINET,
- TTL: 0,
- },
- dnsmessage.AResource{
- A: [4]byte{192, 0, 0, 8},
- },
- )
- }}
- f := filter.NewAllowAllForTest(logger.Discard)
- h.ps.b.setFilter(f)
- if !h.ps.b.OfferingAppConnector() {
- t.Fatal("expecting to be offering app connector")
- }
- if !h.replyToDNSQueries() {
- t.Errorf("unexpectedly deny; wanted to be a DNS server")
- }
- w := httptest.NewRecorder()
- h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=example.com.", nil))
- if w.Code != http.StatusOK {
- t.Errorf("unexpected status code: %v", w.Code)
- }
- a.Wait(t.Context())
- wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
- if !slices.Equal(rc.Routes(), wantRoutes) {
- t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
- }
- if err := eventbustest.Expect(bw,
- eqUpdate(appctype.RouteUpdate{Advertise: mustPrefix("192.0.0.8/32")}),
- ); err != nil {
- t.Error(err)
- }
- }
- }
- func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
- for _, shouldStore := range []bool{false, true} {
- ctx := context.Background()
- var h peerAPIHandler
- h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
- sys := tsd.NewSystemWithBus(eventbustest.NewBus(t))
- bw := eventbustest.NewWatcher(t, sys.Bus.Get())
- ht := health.NewTracker(sys.Bus.Get())
- reg := new(usermetric.Registry)
- rc := &appctest.RouteCollector{}
- eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, sys.Bus.Get(), sys.Set)
- pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
- a := appc.NewAppConnector(appc.Config{
- Logf: t.Logf,
- EventBus: sys.Bus.Get(),
- RouteAdvertiser: rc,
- HasStoredRoutes: shouldStore,
- })
- t.Cleanup(a.Close)
- sys.Set(pm.Store())
- sys.Set(eng)
- b := newTestLocalBackendWithSys(t, sys)
- b.pm = pm
- b.appConnector = a
- h.ps = &peerAPIServer{b: b}
- h.ps.b.appConnector.UpdateDomains([]string{"www.example.com"})
- a.Wait(ctx)
- h.ps.resolver = &fakeResolver{build: func(b *dnsmessage.Builder) {
- b.CNAMEResource(
- dnsmessage.ResourceHeader{
- Name: dnsmessage.MustNewName("www.example.com."),
- Type: dnsmessage.TypeCNAME,
- Class: dnsmessage.ClassINET,
- TTL: 0,
- },
- dnsmessage.CNAMEResource{
- CNAME: dnsmessage.MustNewName("example.com."),
- },
- )
- b.AResource(
- dnsmessage.ResourceHeader{
- Name: dnsmessage.MustNewName("example.com."),
- Type: dnsmessage.TypeA,
- Class: dnsmessage.ClassINET,
- TTL: 0,
- },
- dnsmessage.AResource{
- A: [4]byte{192, 0, 0, 8},
- },
- )
- }}
- f := filter.NewAllowAllForTest(logger.Discard)
- h.ps.b.setFilter(f)
- if !h.ps.b.OfferingAppConnector() {
- t.Fatal("expecting to be offering app connector")
- }
- if !h.replyToDNSQueries() {
- t.Errorf("unexpectedly deny; wanted to be a DNS server")
- }
- w := httptest.NewRecorder()
- h.handleDNSQuery(w, httptest.NewRequest("GET", "/dns-query?q=www.example.com.", nil))
- if w.Code != http.StatusOK {
- t.Errorf("unexpected status code: %v", w.Code)
- }
- a.Wait(ctx)
- wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
- if !slices.Equal(rc.Routes(), wantRoutes) {
- t.Errorf("got %v; want %v", rc.Routes(), wantRoutes)
- }
- if err := eventbustest.Expect(bw,
- eqUpdate(appctype.RouteUpdate{Advertise: mustPrefix("192.0.0.8/32")}),
- ); err != nil {
- t.Error(err)
- }
- }
- }
- type fakeResolver struct {
- build func(*dnsmessage.Builder)
- }
- func (f *fakeResolver) HandlePeerDNSQuery(ctx context.Context, q []byte, from netip.AddrPort, allowName func(name string) bool) (res []byte, err error) {
- b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
- b.EnableCompression()
- b.StartAnswers()
- f.build(&b)
- return b.Finish()
- }
|