appconnector_test.go 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package appc
  4. import (
  5. stdcmp "cmp"
  6. "fmt"
  7. "net/netip"
  8. "reflect"
  9. "slices"
  10. "sync/atomic"
  11. "testing"
  12. "time"
  13. "github.com/google/go-cmp/cmp"
  14. "github.com/google/go-cmp/cmp/cmpopts"
  15. "golang.org/x/net/dns/dnsmessage"
  16. "tailscale.com/appc/appctest"
  17. "tailscale.com/tstest"
  18. "tailscale.com/types/appctype"
  19. "tailscale.com/util/clientmetric"
  20. "tailscale.com/util/eventbus/eventbustest"
  21. "tailscale.com/util/mak"
  22. "tailscale.com/util/must"
  23. "tailscale.com/util/slicesx"
  24. )
  25. func TestUpdateDomains(t *testing.T) {
  26. ctx := t.Context()
  27. bus := eventbustest.NewBus(t)
  28. for _, shouldStore := range []bool{false, true} {
  29. a := NewAppConnector(Config{
  30. Logf: t.Logf,
  31. EventBus: bus,
  32. HasStoredRoutes: shouldStore,
  33. })
  34. t.Cleanup(a.Close)
  35. a.UpdateDomains([]string{"example.com"})
  36. a.Wait(ctx)
  37. if got, want := a.Domains().AsSlice(), []string{"example.com"}; !slices.Equal(got, want) {
  38. t.Errorf("got %v; want %v", got, want)
  39. }
  40. addr := netip.MustParseAddr("192.0.0.8")
  41. a.domains["example.com"] = append(a.domains["example.com"], addr)
  42. a.UpdateDomains([]string{"example.com"})
  43. a.Wait(ctx)
  44. if got, want := a.domains["example.com"], []netip.Addr{addr}; !slices.Equal(got, want) {
  45. t.Errorf("got %v; want %v", got, want)
  46. }
  47. // domains are explicitly downcased on set.
  48. a.UpdateDomains([]string{"UP.EXAMPLE.COM"})
  49. a.Wait(ctx)
  50. if got, want := slicesx.MapKeys(a.domains), []string{"up.example.com"}; !slices.Equal(got, want) {
  51. t.Errorf("got %v; want %v", got, want)
  52. }
  53. }
  54. }
  55. func TestUpdateRoutes(t *testing.T) {
  56. ctx := t.Context()
  57. bus := eventbustest.NewBus(t)
  58. for _, shouldStore := range []bool{false, true} {
  59. w := eventbustest.NewWatcher(t, bus)
  60. rc := &appctest.RouteCollector{}
  61. a := NewAppConnector(Config{
  62. Logf: t.Logf,
  63. EventBus: bus,
  64. RouteAdvertiser: rc,
  65. HasStoredRoutes: shouldStore,
  66. })
  67. t.Cleanup(a.Close)
  68. a.updateDomains([]string{"*.example.com"})
  69. // This route should be collapsed into the range
  70. if err := a.ObserveDNSResponse(dnsResponse("a.example.com.", "192.0.2.1")); err != nil {
  71. t.Errorf("ObserveDNSResponse: %v", err)
  72. }
  73. a.Wait(ctx)
  74. if !slices.Equal(rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}) {
  75. t.Fatalf("got %v, want %v", rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
  76. }
  77. // This route should not be collapsed or removed
  78. if err := a.ObserveDNSResponse(dnsResponse("b.example.com.", "192.0.0.1")); err != nil {
  79. t.Errorf("ObserveDNSResponse: %v", err)
  80. }
  81. a.Wait(ctx)
  82. routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24"), netip.MustParsePrefix("192.0.0.1/32")}
  83. a.updateRoutes(routes)
  84. a.Wait(ctx)
  85. slices.SortFunc(rc.Routes(), prefixCompare)
  86. rc.SetRoutes(slices.Compact(rc.Routes()))
  87. slices.SortFunc(routes, prefixCompare)
  88. // Ensure that the non-matching /32 is preserved, even though it's in the domains table.
  89. if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
  90. t.Errorf("added routes: got %v, want %v", rc.Routes(), routes)
  91. }
  92. // Ensure that the contained /32 is removed, replaced by the /24.
  93. wantRemoved := []netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")}
  94. if !slices.EqualFunc(rc.RemovedRoutes(), wantRemoved, prefixEqual) {
  95. t.Fatalf("unexpected removed routes: %v", rc.RemovedRoutes())
  96. }
  97. if err := eventbustest.Expect(w,
  98. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.2.1/32")}),
  99. eventbustest.Type[appctype.RouteInfo](),
  100. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.1/32")}),
  101. eventbustest.Type[appctype.RouteInfo](),
  102. eqUpdate(appctype.RouteUpdate{
  103. Advertise: prefixes("192.0.0.1/32", "192.0.2.0/24"),
  104. Unadvertise: prefixes("192.0.2.1/32"),
  105. }),
  106. eventbustest.Type[appctype.RouteInfo](),
  107. ); err != nil {
  108. t.Error(err)
  109. }
  110. }
  111. }
  112. func TestUpdateRoutesUnadvertisesContainedRoutes(t *testing.T) {
  113. ctx := t.Context()
  114. bus := eventbustest.NewBus(t)
  115. for _, shouldStore := range []bool{false, true} {
  116. w := eventbustest.NewWatcher(t, bus)
  117. rc := &appctest.RouteCollector{}
  118. a := NewAppConnector(Config{
  119. Logf: t.Logf,
  120. EventBus: bus,
  121. RouteAdvertiser: rc,
  122. HasStoredRoutes: shouldStore,
  123. })
  124. t.Cleanup(a.Close)
  125. mak.Set(&a.domains, "example.com", []netip.Addr{netip.MustParseAddr("192.0.2.1")})
  126. rc.SetRoutes([]netip.Prefix{netip.MustParsePrefix("192.0.2.1/32")})
  127. routes := []netip.Prefix{netip.MustParsePrefix("192.0.2.0/24")}
  128. a.updateRoutes(routes)
  129. a.Wait(ctx)
  130. if !slices.EqualFunc(routes, rc.Routes(), prefixEqual) {
  131. t.Fatalf("got %v, want %v", rc.Routes(), routes)
  132. }
  133. if err := eventbustest.ExpectExactly(w,
  134. eqUpdate(appctype.RouteUpdate{
  135. Advertise: prefixes("192.0.2.0/24"),
  136. Unadvertise: prefixes("192.0.2.1/32"),
  137. }),
  138. eventbustest.Type[appctype.RouteInfo](),
  139. ); err != nil {
  140. t.Error(err)
  141. }
  142. }
  143. }
  144. func TestDomainRoutes(t *testing.T) {
  145. bus := eventbustest.NewBus(t)
  146. for _, shouldStore := range []bool{false, true} {
  147. w := eventbustest.NewWatcher(t, bus)
  148. rc := &appctest.RouteCollector{}
  149. a := NewAppConnector(Config{
  150. Logf: t.Logf,
  151. EventBus: bus,
  152. RouteAdvertiser: rc,
  153. HasStoredRoutes: shouldStore,
  154. })
  155. t.Cleanup(a.Close)
  156. a.updateDomains([]string{"example.com"})
  157. if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil {
  158. t.Errorf("ObserveDNSResponse: %v", err)
  159. }
  160. a.Wait(t.Context())
  161. want := map[string][]netip.Addr{
  162. "example.com": {netip.MustParseAddr("192.0.0.8")},
  163. }
  164. if got := a.DomainRoutes(); !reflect.DeepEqual(got, want) {
  165. t.Fatalf("DomainRoutes: got %v, want %v", got, want)
  166. }
  167. if err := eventbustest.ExpectExactly(w,
  168. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.8/32")}),
  169. eventbustest.Type[appctype.RouteInfo](),
  170. ); err != nil {
  171. t.Error(err)
  172. }
  173. }
  174. }
  175. func TestObserveDNSResponse(t *testing.T) {
  176. ctx := t.Context()
  177. bus := eventbustest.NewBus(t)
  178. for _, shouldStore := range []bool{false, true} {
  179. w := eventbustest.NewWatcher(t, bus)
  180. rc := &appctest.RouteCollector{}
  181. a := NewAppConnector(Config{
  182. Logf: t.Logf,
  183. EventBus: bus,
  184. RouteAdvertiser: rc,
  185. HasStoredRoutes: shouldStore,
  186. })
  187. t.Cleanup(a.Close)
  188. // a has no domains configured, so it should not advertise any routes
  189. if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil {
  190. t.Errorf("ObserveDNSResponse: %v", err)
  191. }
  192. if got, want := rc.Routes(), ([]netip.Prefix)(nil); !slices.Equal(got, want) {
  193. t.Errorf("got %v; want %v", got, want)
  194. }
  195. wantRoutes := []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}
  196. a.updateDomains([]string{"example.com"})
  197. if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.0.8")); err != nil {
  198. t.Errorf("ObserveDNSResponse: %v", err)
  199. }
  200. a.Wait(ctx)
  201. if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
  202. t.Errorf("got %v; want %v", got, want)
  203. }
  204. // a CNAME record chain should result in a route being added if the chain
  205. // matches a routed domain.
  206. a.updateDomains([]string{"www.example.com", "example.com"})
  207. if err := a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.9", "www.example.com.", "chain.example.com.", "example.com.")); err != nil {
  208. t.Errorf("ObserveDNSResponse: %v", err)
  209. }
  210. a.Wait(ctx)
  211. wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.9/32"))
  212. if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
  213. t.Errorf("got %v; want %v", got, want)
  214. }
  215. // a CNAME record chain should result in a route being added if the chain
  216. // even if only found in the middle of the chain
  217. if err := a.ObserveDNSResponse(dnsCNAMEResponse("192.0.0.10", "outside.example.org.", "www.example.com.", "example.org.")); err != nil {
  218. t.Errorf("ObserveDNSResponse: %v", err)
  219. }
  220. a.Wait(ctx)
  221. wantRoutes = append(wantRoutes, netip.MustParsePrefix("192.0.0.10/32"))
  222. if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
  223. t.Errorf("got %v; want %v", got, want)
  224. }
  225. wantRoutes = append(wantRoutes, netip.MustParsePrefix("2001:db8::1/128"))
  226. if err := a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1")); err != nil {
  227. t.Errorf("ObserveDNSResponse: %v", err)
  228. }
  229. a.Wait(ctx)
  230. if got, want := rc.Routes(), wantRoutes; !slices.Equal(got, want) {
  231. t.Errorf("got %v; want %v", got, want)
  232. }
  233. // don't re-advertise routes that have already been advertised
  234. if err := a.ObserveDNSResponse(dnsResponse("example.com.", "2001:db8::1")); err != nil {
  235. t.Errorf("ObserveDNSResponse: %v", err)
  236. }
  237. a.Wait(ctx)
  238. if !slices.Equal(rc.Routes(), wantRoutes) {
  239. t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
  240. }
  241. // don't advertise addresses that are already in a control provided route
  242. pfx := netip.MustParsePrefix("192.0.2.0/24")
  243. a.updateRoutes([]netip.Prefix{pfx})
  244. wantRoutes = append(wantRoutes, pfx)
  245. if err := a.ObserveDNSResponse(dnsResponse("example.com.", "192.0.2.1")); err != nil {
  246. t.Errorf("ObserveDNSResponse: %v", err)
  247. }
  248. a.Wait(ctx)
  249. if !slices.Equal(rc.Routes(), wantRoutes) {
  250. t.Errorf("rc.Routes(): got %v; want %v", rc.Routes(), wantRoutes)
  251. }
  252. if !slices.Contains(a.domains["example.com"], netip.MustParseAddr("192.0.2.1")) {
  253. t.Errorf("missing %v from %v", "192.0.2.1", a.domains["exmaple.com"])
  254. }
  255. if err := eventbustest.ExpectExactly(w,
  256. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.8/32")}), // from initial DNS response, via example.com
  257. eventbustest.Type[appctype.RouteInfo](),
  258. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.9/32")}), // from CNAME response
  259. eventbustest.Type[appctype.RouteInfo](),
  260. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.10/32")}), // from CNAME response, mid-chain
  261. eventbustest.Type[appctype.RouteInfo](),
  262. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("2001:db8::1/128")}), // v6 DNS response
  263. eventbustest.Type[appctype.RouteInfo](),
  264. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.2.0/24")}), // additional prefix
  265. eventbustest.Type[appctype.RouteInfo](),
  266. // N.B. no update for 192.0.2.1 as it is already covered
  267. ); err != nil {
  268. t.Error(err)
  269. }
  270. }
  271. }
  272. func TestWildcardDomains(t *testing.T) {
  273. ctx := t.Context()
  274. bus := eventbustest.NewBus(t)
  275. for _, shouldStore := range []bool{false, true} {
  276. w := eventbustest.NewWatcher(t, bus)
  277. rc := &appctest.RouteCollector{}
  278. a := NewAppConnector(Config{
  279. Logf: t.Logf,
  280. EventBus: bus,
  281. RouteAdvertiser: rc,
  282. HasStoredRoutes: shouldStore,
  283. })
  284. t.Cleanup(a.Close)
  285. a.updateDomains([]string{"*.example.com"})
  286. if err := a.ObserveDNSResponse(dnsResponse("foo.example.com.", "192.0.0.8")); err != nil {
  287. t.Errorf("ObserveDNSResponse: %v", err)
  288. }
  289. a.Wait(ctx)
  290. if got, want := rc.Routes(), []netip.Prefix{netip.MustParsePrefix("192.0.0.8/32")}; !slices.Equal(got, want) {
  291. t.Errorf("routes: got %v; want %v", got, want)
  292. }
  293. if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
  294. t.Errorf("wildcards: got %v; want %v", got, want)
  295. }
  296. a.updateDomains([]string{"*.example.com", "example.com"})
  297. if _, ok := a.domains["foo.example.com"]; !ok {
  298. t.Errorf("expected foo.example.com to be preserved in domains due to wildcard")
  299. }
  300. if got, want := a.wildcards, []string{"example.com"}; !slices.Equal(got, want) {
  301. t.Errorf("wildcards: got %v; want %v", got, want)
  302. }
  303. // There was an early regression where the wildcard domain was added repeatedly, this guards against that.
  304. a.updateDomains([]string{"*.example.com", "example.com"})
  305. if len(a.wildcards) != 1 {
  306. t.Errorf("expected only one wildcard domain, got %v", a.wildcards)
  307. }
  308. if err := eventbustest.ExpectExactly(w,
  309. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("192.0.0.8/32")}),
  310. eventbustest.Type[appctype.RouteInfo](),
  311. ); err != nil {
  312. t.Error(err)
  313. }
  314. }
  315. }
  316. // dnsResponse is a test helper that creates a DNS response buffer for the given domain and address
  317. func dnsResponse(domain, address string) []byte {
  318. addr := netip.MustParseAddr(address)
  319. b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
  320. b.EnableCompression()
  321. b.StartAnswers()
  322. switch addr.BitLen() {
  323. case 32:
  324. b.AResource(
  325. dnsmessage.ResourceHeader{
  326. Name: dnsmessage.MustNewName(domain),
  327. Type: dnsmessage.TypeA,
  328. Class: dnsmessage.ClassINET,
  329. TTL: 0,
  330. },
  331. dnsmessage.AResource{
  332. A: addr.As4(),
  333. },
  334. )
  335. case 128:
  336. b.AAAAResource(
  337. dnsmessage.ResourceHeader{
  338. Name: dnsmessage.MustNewName(domain),
  339. Type: dnsmessage.TypeAAAA,
  340. Class: dnsmessage.ClassINET,
  341. TTL: 0,
  342. },
  343. dnsmessage.AAAAResource{
  344. AAAA: addr.As16(),
  345. },
  346. )
  347. default:
  348. panic("invalid address length")
  349. }
  350. return must.Get(b.Finish())
  351. }
  352. func dnsCNAMEResponse(address string, domains ...string) []byte {
  353. addr := netip.MustParseAddr(address)
  354. b := dnsmessage.NewBuilder(nil, dnsmessage.Header{})
  355. b.EnableCompression()
  356. b.StartAnswers()
  357. if len(domains) >= 2 {
  358. for i, domain := range domains[:len(domains)-1] {
  359. b.CNAMEResource(
  360. dnsmessage.ResourceHeader{
  361. Name: dnsmessage.MustNewName(domain),
  362. Type: dnsmessage.TypeCNAME,
  363. Class: dnsmessage.ClassINET,
  364. TTL: 0,
  365. },
  366. dnsmessage.CNAMEResource{
  367. CNAME: dnsmessage.MustNewName(domains[i+1]),
  368. },
  369. )
  370. }
  371. }
  372. domain := domains[len(domains)-1]
  373. switch addr.BitLen() {
  374. case 32:
  375. b.AResource(
  376. dnsmessage.ResourceHeader{
  377. Name: dnsmessage.MustNewName(domain),
  378. Type: dnsmessage.TypeA,
  379. Class: dnsmessage.ClassINET,
  380. TTL: 0,
  381. },
  382. dnsmessage.AResource{
  383. A: addr.As4(),
  384. },
  385. )
  386. case 128:
  387. b.AAAAResource(
  388. dnsmessage.ResourceHeader{
  389. Name: dnsmessage.MustNewName(domain),
  390. Type: dnsmessage.TypeAAAA,
  391. Class: dnsmessage.ClassINET,
  392. TTL: 0,
  393. },
  394. dnsmessage.AAAAResource{
  395. AAAA: addr.As16(),
  396. },
  397. )
  398. default:
  399. panic("invalid address length")
  400. }
  401. return must.Get(b.Finish())
  402. }
  403. func prefixEqual(a, b netip.Prefix) bool {
  404. return a == b
  405. }
  406. func prefixCompare(a, b netip.Prefix) int {
  407. if a.Addr().Compare(b.Addr()) == 0 {
  408. return a.Bits() - b.Bits()
  409. }
  410. return a.Addr().Compare(b.Addr())
  411. }
  412. func prefixes(in ...string) []netip.Prefix {
  413. toRet := make([]netip.Prefix, len(in))
  414. for i, s := range in {
  415. toRet[i] = netip.MustParsePrefix(s)
  416. }
  417. return toRet
  418. }
  419. func TestUpdateRouteRouteRemoval(t *testing.T) {
  420. ctx := t.Context()
  421. bus := eventbustest.NewBus(t)
  422. for _, shouldStore := range []bool{false, true} {
  423. w := eventbustest.NewWatcher(t, bus)
  424. rc := &appctest.RouteCollector{}
  425. assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
  426. if !slices.Equal(routes, rc.Routes()) {
  427. t.Fatalf("%s: (shouldStore=%t) routes want %v, got %v", prefix, shouldStore, routes, rc.Routes())
  428. }
  429. if !slices.Equal(removedRoutes, rc.RemovedRoutes()) {
  430. t.Fatalf("%s: (shouldStore=%t) removedRoutes want %v, got %v", prefix, shouldStore, removedRoutes, rc.RemovedRoutes())
  431. }
  432. }
  433. a := NewAppConnector(Config{
  434. Logf: t.Logf,
  435. EventBus: bus,
  436. RouteAdvertiser: rc,
  437. HasStoredRoutes: shouldStore,
  438. })
  439. t.Cleanup(a.Close)
  440. // nothing has yet been advertised
  441. assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
  442. a.UpdateDomainsAndRoutes([]string{}, prefixes("1.2.3.1/32", "1.2.3.2/32"))
  443. a.Wait(ctx)
  444. // the routes passed to UpdateDomainsAndRoutes have been advertised
  445. assertRoutes("simple update", prefixes("1.2.3.1/32", "1.2.3.2/32"), []netip.Prefix{})
  446. // one route the same, one different
  447. a.UpdateDomainsAndRoutes([]string{}, prefixes("1.2.3.1/32", "1.2.3.3/32"))
  448. a.Wait(ctx)
  449. // old behavior: routes are not removed, resulting routes are both old and new
  450. // (we have dupe 1.2.3.1 routes because the test RouteAdvertiser doesn't have the deduplication
  451. // the real one does)
  452. wantRoutes := prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.1/32", "1.2.3.3/32")
  453. wantRemovedRoutes := []netip.Prefix{}
  454. if shouldStore {
  455. // new behavior: routes are removed, resulting routes are new only
  456. wantRoutes = prefixes("1.2.3.1/32", "1.2.3.1/32", "1.2.3.3/32")
  457. wantRemovedRoutes = prefixes("1.2.3.2/32")
  458. }
  459. assertRoutes("removal", wantRoutes, wantRemovedRoutes)
  460. if err := eventbustest.Expect(w,
  461. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.1/32", "1.2.3.2/32")}), // no duplicates here
  462. eventbustest.Type[appctype.RouteInfo](),
  463. ); err != nil {
  464. t.Error(err)
  465. }
  466. }
  467. }
  468. func TestUpdateDomainRouteRemoval(t *testing.T) {
  469. ctx := t.Context()
  470. bus := eventbustest.NewBus(t)
  471. for _, shouldStore := range []bool{false, true} {
  472. w := eventbustest.NewWatcher(t, bus)
  473. rc := &appctest.RouteCollector{}
  474. assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
  475. if !slices.Equal(routes, rc.Routes()) {
  476. t.Fatalf("%s: (shouldStore=%t) routes want %v, got %v", prefix, shouldStore, routes, rc.Routes())
  477. }
  478. if !slices.Equal(removedRoutes, rc.RemovedRoutes()) {
  479. t.Fatalf("%s: (shouldStore=%t) removedRoutes want %v, got %v", prefix, shouldStore, removedRoutes, rc.RemovedRoutes())
  480. }
  481. }
  482. a := NewAppConnector(Config{
  483. Logf: t.Logf,
  484. EventBus: bus,
  485. RouteAdvertiser: rc,
  486. HasStoredRoutes: shouldStore,
  487. })
  488. t.Cleanup(a.Close)
  489. assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
  490. a.UpdateDomainsAndRoutes([]string{"a.example.com", "b.example.com"}, []netip.Prefix{})
  491. a.Wait(ctx)
  492. // adding domains doesn't immediately cause any routes to be advertised
  493. assertRoutes("update domains", []netip.Prefix{}, []netip.Prefix{})
  494. for _, res := range [][]byte{
  495. dnsResponse("a.example.com.", "1.2.3.1"),
  496. dnsResponse("a.example.com.", "1.2.3.2"),
  497. dnsResponse("b.example.com.", "1.2.3.3"),
  498. dnsResponse("b.example.com.", "1.2.3.4"),
  499. } {
  500. if err := a.ObserveDNSResponse(res); err != nil {
  501. t.Errorf("ObserveDNSResponse: %v", err)
  502. }
  503. }
  504. a.Wait(ctx)
  505. // observing dns responses causes routes to be advertised
  506. assertRoutes("observed dns", prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32"), []netip.Prefix{})
  507. a.UpdateDomainsAndRoutes([]string{"a.example.com"}, []netip.Prefix{})
  508. a.Wait(ctx)
  509. // old behavior, routes are not removed
  510. wantRoutes := prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32")
  511. wantRemovedRoutes := []netip.Prefix{}
  512. if shouldStore {
  513. // new behavior, routes are removed for b.example.com
  514. wantRoutes = prefixes("1.2.3.1/32", "1.2.3.2/32")
  515. wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32")
  516. }
  517. assertRoutes("removal", wantRoutes, wantRemovedRoutes)
  518. wantEvents := []any{
  519. // Each DNS record observed triggers an update.
  520. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.1/32")}),
  521. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.2/32")}),
  522. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.3/32")}),
  523. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.4/32")}),
  524. }
  525. if shouldStore {
  526. wantEvents = append(wantEvents, eqUpdate(appctype.RouteUpdate{
  527. Unadvertise: prefixes("1.2.3.3/32", "1.2.3.4/32"),
  528. }))
  529. }
  530. if err := eventbustest.Expect(w, wantEvents...); err != nil {
  531. t.Error(err)
  532. }
  533. }
  534. }
  535. func TestUpdateWildcardRouteRemoval(t *testing.T) {
  536. ctx := t.Context()
  537. bus := eventbustest.NewBus(t)
  538. for _, shouldStore := range []bool{false, true} {
  539. w := eventbustest.NewWatcher(t, bus)
  540. rc := &appctest.RouteCollector{}
  541. assertRoutes := func(prefix string, routes, removedRoutes []netip.Prefix) {
  542. if !slices.Equal(routes, rc.Routes()) {
  543. t.Fatalf("%s: (shouldStore=%t) routes want %v, got %v", prefix, shouldStore, routes, rc.Routes())
  544. }
  545. if !slices.Equal(removedRoutes, rc.RemovedRoutes()) {
  546. t.Fatalf("%s: (shouldStore=%t) removedRoutes want %v, got %v", prefix, shouldStore, removedRoutes, rc.RemovedRoutes())
  547. }
  548. }
  549. a := NewAppConnector(Config{
  550. Logf: t.Logf,
  551. EventBus: bus,
  552. RouteAdvertiser: rc,
  553. HasStoredRoutes: shouldStore,
  554. })
  555. t.Cleanup(a.Close)
  556. assertRoutes("appc init", []netip.Prefix{}, []netip.Prefix{})
  557. a.UpdateDomainsAndRoutes([]string{"a.example.com", "*.b.example.com"}, []netip.Prefix{})
  558. a.Wait(ctx)
  559. // adding domains doesn't immediately cause any routes to be advertised
  560. assertRoutes("update domains", []netip.Prefix{}, []netip.Prefix{})
  561. for _, res := range [][]byte{
  562. dnsResponse("a.example.com.", "1.2.3.1"),
  563. dnsResponse("a.example.com.", "1.2.3.2"),
  564. dnsResponse("1.b.example.com.", "1.2.3.3"),
  565. dnsResponse("2.b.example.com.", "1.2.3.4"),
  566. } {
  567. if err := a.ObserveDNSResponse(res); err != nil {
  568. t.Errorf("ObserveDNSResponse: %v", err)
  569. }
  570. }
  571. a.Wait(ctx)
  572. // observing dns responses causes routes to be advertised
  573. assertRoutes("observed dns", prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32"), []netip.Prefix{})
  574. a.UpdateDomainsAndRoutes([]string{"a.example.com"}, []netip.Prefix{})
  575. a.Wait(ctx)
  576. // old behavior, routes are not removed
  577. wantRoutes := prefixes("1.2.3.1/32", "1.2.3.2/32", "1.2.3.3/32", "1.2.3.4/32")
  578. wantRemovedRoutes := []netip.Prefix{}
  579. if shouldStore {
  580. // new behavior, routes are removed for *.b.example.com
  581. wantRoutes = prefixes("1.2.3.1/32", "1.2.3.2/32")
  582. wantRemovedRoutes = prefixes("1.2.3.3/32", "1.2.3.4/32")
  583. }
  584. assertRoutes("removal", wantRoutes, wantRemovedRoutes)
  585. wantEvents := []any{
  586. // Each DNS record observed triggers an update.
  587. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.1/32")}),
  588. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.2/32")}),
  589. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.3/32")}),
  590. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("1.2.3.4/32")}),
  591. }
  592. if shouldStore {
  593. wantEvents = append(wantEvents, eqUpdate(appctype.RouteUpdate{
  594. Unadvertise: prefixes("1.2.3.3/32", "1.2.3.4/32"),
  595. }))
  596. }
  597. if err := eventbustest.Expect(w, wantEvents...); err != nil {
  598. t.Error(err)
  599. }
  600. }
  601. }
  602. func TestRoutesWithout(t *testing.T) {
  603. assert := func(msg string, got, want []netip.Prefix) {
  604. if !slices.Equal(want, got) {
  605. t.Errorf("%s: want %v, got %v", msg, want, got)
  606. }
  607. }
  608. assert("empty routes", routesWithout([]netip.Prefix{}, []netip.Prefix{}), []netip.Prefix{})
  609. assert("a empty", routesWithout([]netip.Prefix{}, prefixes("1.1.1.1/32", "1.1.1.2/32")), []netip.Prefix{})
  610. assert("b empty", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), []netip.Prefix{}), prefixes("1.1.1.1/32", "1.1.1.2/32"))
  611. assert("no overlap", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), prefixes("1.1.1.3/32", "1.1.1.4/32")), prefixes("1.1.1.1/32", "1.1.1.2/32"))
  612. assert("a has fewer", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32"), prefixes("1.1.1.1/32", "1.1.1.2/32", "1.1.1.3/32", "1.1.1.4/32")), []netip.Prefix{})
  613. assert("a has more", routesWithout(prefixes("1.1.1.1/32", "1.1.1.2/32", "1.1.1.3/32", "1.1.1.4/32"), prefixes("1.1.1.1/32", "1.1.1.3/32")), prefixes("1.1.1.2/32", "1.1.1.4/32"))
  614. }
  615. func TestRateLogger(t *testing.T) {
  616. clock := tstest.Clock{}
  617. wasCalled := false
  618. rl := newRateLogger(func() time.Time { return clock.Now() }, 1*time.Second, func(count int64, _ time.Time, _ int64) {
  619. if count != 3 {
  620. t.Fatalf("count for prev period: got %d, want 3", count)
  621. }
  622. wasCalled = true
  623. })
  624. for i := 0; i < 3; i++ {
  625. clock.Advance(1 * time.Millisecond)
  626. rl.update(0)
  627. if wasCalled {
  628. t.Fatalf("wasCalled: got true, want false")
  629. }
  630. }
  631. clock.Advance(1 * time.Second)
  632. rl.update(0)
  633. if !wasCalled {
  634. t.Fatalf("wasCalled: got false, want true")
  635. }
  636. wasCalled = false
  637. rl = newRateLogger(func() time.Time { return clock.Now() }, 1*time.Hour, func(count int64, _ time.Time, _ int64) {
  638. if count != 3 {
  639. t.Fatalf("count for prev period: got %d, want 3", count)
  640. }
  641. wasCalled = true
  642. })
  643. for i := 0; i < 3; i++ {
  644. clock.Advance(1 * time.Minute)
  645. rl.update(0)
  646. if wasCalled {
  647. t.Fatalf("wasCalled: got true, want false")
  648. }
  649. }
  650. clock.Advance(1 * time.Hour)
  651. rl.update(0)
  652. if !wasCalled {
  653. t.Fatalf("wasCalled: got false, want true")
  654. }
  655. }
  656. func TestRouteStoreMetrics(t *testing.T) {
  657. metricStoreRoutes(1, 1)
  658. metricStoreRoutes(1, 1) // the 1 buckets value should be 2
  659. metricStoreRoutes(5, 5) // the 5 buckets value should be 1
  660. metricStoreRoutes(6, 6) // the 10 buckets value should be 1
  661. metricStoreRoutes(10001, 10001) // the over buckets value should be 1
  662. wanted := map[string]int64{
  663. "appc_store_routes_n_routes_1": 2,
  664. "appc_store_routes_rate_1": 2,
  665. "appc_store_routes_n_routes_5": 1,
  666. "appc_store_routes_rate_5": 1,
  667. "appc_store_routes_n_routes_10": 1,
  668. "appc_store_routes_rate_10": 1,
  669. "appc_store_routes_n_routes_over": 1,
  670. "appc_store_routes_rate_over": 1,
  671. }
  672. for _, x := range clientmetric.Metrics() {
  673. if x.Value() != wanted[x.Name()] {
  674. t.Errorf("%s: want: %d, got: %d", x.Name(), wanted[x.Name()], x.Value())
  675. }
  676. }
  677. }
  678. func TestMetricBucketsAreSorted(t *testing.T) {
  679. if !slices.IsSorted(metricStoreRoutesRateBuckets) {
  680. t.Errorf("metricStoreRoutesRateBuckets must be in order")
  681. }
  682. if !slices.IsSorted(metricStoreRoutesNBuckets) {
  683. t.Errorf("metricStoreRoutesNBuckets must be in order")
  684. }
  685. }
  686. // TestUpdateRoutesDeadlock is a regression test for a deadlock in
  687. // LocalBackend<->AppConnector interaction. When using real LocalBackend as the
  688. // routeAdvertiser, calls to Advertise/UnadvertiseRoutes can end up calling
  689. // back into AppConnector via authReconfig. If everything is called
  690. // synchronously, this results in a deadlock on AppConnector.mu.
  691. //
  692. // TODO(creachadair, 2025-09-18): Remove this along with the advertiser
  693. // interface once the LocalBackend is switched to use the event bus and the
  694. // tests have been updated not to need it.
  695. func TestUpdateRoutesDeadlock(t *testing.T) {
  696. ctx := t.Context()
  697. bus := eventbustest.NewBus(t)
  698. w := eventbustest.NewWatcher(t, bus)
  699. rc := &appctest.RouteCollector{}
  700. a := NewAppConnector(Config{
  701. Logf: t.Logf,
  702. EventBus: bus,
  703. RouteAdvertiser: rc,
  704. HasStoredRoutes: true,
  705. })
  706. t.Cleanup(a.Close)
  707. advertiseCalled := new(atomic.Bool)
  708. unadvertiseCalled := new(atomic.Bool)
  709. rc.AdvertiseCallback = func() {
  710. // Call something that requires a.mu to be held.
  711. a.DomainRoutes()
  712. advertiseCalled.Store(true)
  713. }
  714. rc.UnadvertiseCallback = func() {
  715. // Call something that requires a.mu to be held.
  716. a.DomainRoutes()
  717. unadvertiseCalled.Store(true)
  718. }
  719. a.updateDomains([]string{"example.com"})
  720. a.Wait(ctx)
  721. // Trigger rc.AdveriseRoute.
  722. a.updateRoutes(
  723. []netip.Prefix{
  724. netip.MustParsePrefix("127.0.0.1/32"),
  725. netip.MustParsePrefix("127.0.0.2/32"),
  726. },
  727. )
  728. a.Wait(ctx)
  729. // Trigger rc.UnadveriseRoute.
  730. a.updateRoutes(
  731. []netip.Prefix{
  732. netip.MustParsePrefix("127.0.0.1/32"),
  733. },
  734. )
  735. a.Wait(ctx)
  736. if !advertiseCalled.Load() {
  737. t.Error("AdvertiseRoute was not called")
  738. }
  739. if !unadvertiseCalled.Load() {
  740. t.Error("UnadvertiseRoute was not called")
  741. }
  742. if want := []netip.Prefix{netip.MustParsePrefix("127.0.0.1/32")}; !slices.Equal(slices.Compact(rc.Routes()), want) {
  743. t.Fatalf("got %v, want %v", rc.Routes(), want)
  744. }
  745. if err := eventbustest.ExpectExactly(w,
  746. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("127.0.0.1/32", "127.0.0.2/32")}),
  747. eventbustest.Type[appctype.RouteInfo](),
  748. eqUpdate(appctype.RouteUpdate{Advertise: prefixes("127.0.0.1/32"), Unadvertise: prefixes("127.0.0.2/32")}),
  749. eventbustest.Type[appctype.RouteInfo](),
  750. ); err != nil {
  751. t.Error(err)
  752. }
  753. }
  754. type textUpdate struct {
  755. Advertise []string
  756. Unadvertise []string
  757. }
  758. func routeUpdateToText(u appctype.RouteUpdate) textUpdate {
  759. var out textUpdate
  760. for _, p := range u.Advertise {
  761. out.Advertise = append(out.Advertise, p.String())
  762. }
  763. for _, p := range u.Unadvertise {
  764. out.Unadvertise = append(out.Unadvertise, p.String())
  765. }
  766. return out
  767. }
  768. // eqUpdate generates an eventbus test filter that matches a appctype.RouteUpdate
  769. // message equal to want, or reports an error giving a human-readable diff.
  770. func eqUpdate(want appctype.RouteUpdate) func(appctype.RouteUpdate) error {
  771. return func(got appctype.RouteUpdate) error {
  772. if diff := cmp.Diff(routeUpdateToText(got), routeUpdateToText(want),
  773. cmpopts.SortSlices(stdcmp.Less[string]),
  774. ); diff != "" {
  775. return fmt.Errorf("wrong update (-got, +want):\n%s", diff)
  776. }
  777. return nil
  778. }
  779. }