manager_linux_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package dns
  4. import (
  5. "errors"
  6. "io/fs"
  7. "os"
  8. "strings"
  9. "testing"
  10. "tailscale.com/tstest"
  11. "tailscale.com/util/cmpver"
  12. )
  13. func TestLinuxDNSMode(t *testing.T) {
  14. tests := []struct {
  15. name string
  16. env newOSConfigEnv
  17. wantLog string
  18. want string
  19. }{
  20. {
  21. name: "no_obvious_resolv.conf_owner",
  22. env: env(resolvDotConf("nameserver 10.0.0.1")),
  23. wantLog: "dns: [rc=unknown ret=direct]",
  24. want: "direct",
  25. },
  26. {
  27. name: "network_manager",
  28. env: env(
  29. resolvDotConf(
  30. "# Managed by NetworkManager",
  31. "nameserver 10.0.0.1")),
  32. wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
  33. "dns: [rc=nm resolved=not-in-use ret=direct]",
  34. want: "direct",
  35. },
  36. {
  37. name: "resolvconf_but_no_resolvconf_binary",
  38. env: env(resolvDotConf("# Managed by resolvconf", "nameserver 10.0.0.1")),
  39. wantLog: "dns: [rc=resolvconf resolvconf=no ret=direct]",
  40. want: "direct",
  41. },
  42. {
  43. name: "debian_resolvconf",
  44. env: env(
  45. resolvDotConf("# Managed by resolvconf", "nameserver 10.0.0.1"),
  46. resolvconf("debian")),
  47. wantLog: "dns: [rc=resolvconf resolvconf=debian ret=debian-resolvconf]",
  48. want: "debian-resolvconf",
  49. },
  50. {
  51. name: "openresolv",
  52. env: env(
  53. resolvDotConf("# Managed by resolvconf", "nameserver 10.0.0.1"),
  54. resolvconf("openresolv")),
  55. wantLog: "dns: [rc=resolvconf resolvconf=openresolv ret=openresolv]",
  56. want: "openresolv",
  57. },
  58. {
  59. name: "unknown_resolvconf_flavor",
  60. env: env(
  61. resolvDotConf("# Managed by resolvconf", "nameserver 10.0.0.1"),
  62. resolvconf("daves-discount-resolvconf")),
  63. wantLog: "[unexpected] got unknown flavor of resolvconf \"daves-discount-resolvconf\", falling back to direct manager\ndns: [rc=resolvconf resolvconf=daves-discount-resolvconf ret=direct]",
  64. want: "direct",
  65. },
  66. {
  67. name: "resolved_alone_without_ping",
  68. env: env(resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53")),
  69. wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]",
  70. want: "systemd-resolved",
  71. },
  72. {
  73. name: "resolved_alone_with_ping",
  74. env: env(
  75. resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
  76. resolvedRunning()),
  77. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  78. want: "systemd-resolved",
  79. },
  80. {
  81. name: "resolved_and_nsswitch_resolve",
  82. env: env(
  83. resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
  84. resolvedRunning(),
  85. nsswitchDotConf("hosts: files resolve [!UNAVAIL=return] dns"),
  86. ),
  87. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=nss nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  88. want: "systemd-resolved",
  89. },
  90. {
  91. name: "resolved_and_nsswitch_dns",
  92. env: env(
  93. resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
  94. resolvedRunning(),
  95. nsswitchDotConf("hosts: files dns resolve [!UNAVAIL=return]"),
  96. ),
  97. wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]",
  98. want: "direct",
  99. },
  100. {
  101. name: "resolved_and_nsswitch_none",
  102. env: env(
  103. resolvDotConf("# Managed by systemd-resolved", "nameserver 1.1.1.1"),
  104. resolvedRunning(),
  105. nsswitchDotConf("hosts:"),
  106. ),
  107. wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [1.1.1.1]\ndns: [resolved-ping=yes rc=resolved resolved=not-in-use ret=direct]",
  108. want: "direct",
  109. },
  110. {
  111. name: "resolved_and_networkmanager_not_using_resolved",
  112. env: env(
  113. resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
  114. resolvedRunning(),
  115. nmRunning("1.2.3", false)),
  116. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=no resolv-conf-mode=fortests ret=systemd-resolved]",
  117. want: "systemd-resolved",
  118. },
  119. {
  120. name: "resolved_and_mid_2020_networkmanager",
  121. env: env(
  122. resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
  123. resolvedRunning(),
  124. nmRunning("1.26.2", true)),
  125. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=yes ret=network-manager]",
  126. want: "network-manager",
  127. },
  128. {
  129. name: "resolved_and_2021_networkmanager",
  130. env: env(
  131. resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
  132. resolvedRunning(),
  133. nmRunning("1.27.0", true)),
  134. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
  135. want: "systemd-resolved",
  136. },
  137. {
  138. name: "resolved_and_ancient_networkmanager",
  139. env: env(
  140. resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
  141. resolvedRunning(),
  142. nmRunning("1.22.0", true)),
  143. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=yes nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
  144. want: "systemd-resolved",
  145. },
  146. // Regression tests for extreme corner cases below.
  147. {
  148. // One user reported a configuration whose comment string
  149. // alleged that it was managed by systemd-resolved, but it
  150. // was actually a completely static config file pointing
  151. // elsewhere.
  152. name: "allegedly_resolved_but_not_in_resolv.conf",
  153. env: env(resolvDotConf("# Managed by systemd-resolved", "nameserver 10.0.0.1")),
  154. wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
  155. "dns: [rc=resolved resolved=not-in-use ret=direct]",
  156. want: "direct",
  157. },
  158. {
  159. // We used to incorrectly decide that resolved wasn't in
  160. // charge when handed this (admittedly weird and bugged)
  161. // resolv.conf.
  162. name: "resolved_with_duplicates_in_resolv.conf",
  163. env: env(
  164. resolvDotConf(
  165. "# Managed by systemd-resolved",
  166. "nameserver 127.0.0.53",
  167. "nameserver 127.0.0.53"),
  168. resolvedRunning()),
  169. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  170. want: "systemd-resolved",
  171. },
  172. {
  173. // More than one user has had resolvconf write a config that points to
  174. // systemd-resolved. We're better off using systemd-resolved.
  175. // regression test for https://github.com/tailscale/tailscale/issues/3026
  176. name: "allegedly_resolvconf_but_actually_systemd-resolved",
  177. env: env(resolvDotConf(
  178. "# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)",
  179. "# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN",
  180. "# 127.0.0.53 is the systemd-resolved stub resolver.",
  181. "# run \"systemd-resolve --status\" to see details about the actual nameservers.",
  182. "nameserver 127.0.0.53"),
  183. resolvedRunning()),
  184. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  185. want: "systemd-resolved",
  186. },
  187. {
  188. // More than one user has had resolvconf write a config that points to
  189. // systemd-resolved. We're better off using systemd-resolved.
  190. // and assuming that even if the ping doesn't show that env is correct
  191. // regression test for https://github.com/tailscale/tailscale/issues/3026
  192. name: "allegedly_resolvconf_but_actually_systemd-resolved_but_no_ping",
  193. env: env(resolvDotConf(
  194. "# Dynamic resolv.conf(5) file for glibc resolver(3) generated by resolvconf(8)",
  195. "# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN",
  196. "# 127.0.0.53 is the systemd-resolved stub resolver.",
  197. "# run \"systemd-resolve --status\" to see details about the actual nameservers.",
  198. "nameserver 127.0.0.53")),
  199. wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=resolved resolved=file nm=no resolv-conf-mode=error ret=systemd-resolved]",
  200. want: "systemd-resolved",
  201. },
  202. {
  203. // regression test for https://github.com/tailscale/tailscale/issues/3304
  204. name: "networkmanager_but_pointing_at_systemd-resolved",
  205. env: env(resolvDotConf(
  206. "# Generated by NetworkManager",
  207. "nameserver 127.0.0.53",
  208. "options edns0 trust-ad"),
  209. resolvedRunning(),
  210. nmRunning("1.32.12", true)),
  211. wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=fortests ret=systemd-resolved]",
  212. want: "systemd-resolved",
  213. },
  214. {
  215. // regression test for https://github.com/tailscale/tailscale/issues/3304
  216. name: "networkmanager_but_pointing_at_systemd-resolved_but_no_resolved_ping",
  217. env: env(resolvDotConf(
  218. "# Generated by NetworkManager",
  219. "nameserver 127.0.0.53",
  220. "options edns0 trust-ad"),
  221. nmRunning("1.32.12", true)),
  222. wantLog: "dns: ResolvConfMode error: dbus property not found\ndns: [rc=nm resolved=file nm-resolved=yes nm-safe=no resolv-conf-mode=error ret=systemd-resolved]",
  223. want: "systemd-resolved",
  224. },
  225. {
  226. // regression test for https://github.com/tailscale/tailscale/issues/3304
  227. name: "networkmanager_but_pointing_at_systemd-resolved_and_safe_nm",
  228. env: env(resolvDotConf(
  229. "# Generated by NetworkManager",
  230. "nameserver 127.0.0.53",
  231. "options edns0 trust-ad"),
  232. resolvedRunning(),
  233. nmRunning("1.26.3", true)),
  234. wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm-safe=yes ret=network-manager]",
  235. want: "network-manager",
  236. },
  237. {
  238. // regression test for https://github.com/tailscale/tailscale/issues/3304
  239. name: "networkmanager_but_pointing_at_systemd-resolved_and_no_networkmanager",
  240. env: env(resolvDotConf(
  241. "# Generated by NetworkManager",
  242. "nameserver 127.0.0.53",
  243. "options edns0 trust-ad"),
  244. resolvedRunning()),
  245. wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  246. want: "systemd-resolved",
  247. },
  248. {
  249. // regression test for https://github.com/tailscale/tailscale/issues/3531
  250. name: "networkmanager_but_systemd-resolved_with_search_domain",
  251. env: env(resolvDotConf(
  252. "# Generated by NetworkManager",
  253. "search lan",
  254. "nameserver 127.0.0.53"),
  255. resolvedRunning()),
  256. wantLog: "dns: [resolved-ping=yes rc=nm resolved=file nm-resolved=yes nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  257. want: "systemd-resolved",
  258. },
  259. {
  260. // Make sure that we ping systemd-resolved to let it start up and write its resolv.conf
  261. // before we read its file.
  262. env: env(resolvedStartOnPingAndThen(
  263. resolvDotConf("# Managed by systemd-resolved", "nameserver 127.0.0.53"),
  264. resolvedDbusProperty(),
  265. )),
  266. wantLog: "dns: [resolved-ping=yes rc=resolved resolved=file nm=no resolv-conf-mode=fortests ret=systemd-resolved]",
  267. want: "systemd-resolved",
  268. },
  269. {
  270. // regression test for https://github.com/tailscale/tailscale/issues/9687
  271. name: "networkmanager_endeavouros",
  272. env: env(resolvDotConf(
  273. "# Generated by NetworkManager",
  274. "search example.com localdomain",
  275. "nameserver 10.0.0.1"),
  276. nmRunning("1.44.2", false)),
  277. wantLog: "dns: resolvedIsActuallyResolver error: resolv.conf doesn't point to systemd-resolved; points to [10.0.0.1]\n" +
  278. "dns: [rc=nm resolved=not-in-use ret=direct]",
  279. want: "direct",
  280. },
  281. }
  282. for _, tt := range tests {
  283. t.Run(tt.name, func(t *testing.T) {
  284. var logBuf tstest.MemLogger
  285. got, err := dnsMode(logBuf.Logf, nil, tt.env)
  286. if err != nil {
  287. t.Fatal(err)
  288. }
  289. if got != tt.want {
  290. t.Errorf("got %s; want %s", got, tt.want)
  291. }
  292. if got := strings.TrimSpace(logBuf.String()); got != tt.wantLog {
  293. t.Errorf("log output mismatch:\n got: %q\nwant: %q\n", got, tt.wantLog)
  294. }
  295. })
  296. }
  297. }
  298. type memFS map[string]any // full path => string for regular files
  299. func (m memFS) Stat(name string) (isRegular bool, err error) {
  300. v, ok := m[name]
  301. if !ok {
  302. return false, fs.ErrNotExist
  303. }
  304. if _, ok := v.(string); ok {
  305. return true, nil
  306. }
  307. return false, nil
  308. }
  309. func (m memFS) Chmod(name string, mode os.FileMode) error { panic("TODO") }
  310. func (m memFS) Rename(oldName, newName string) error { panic("TODO") }
  311. func (m memFS) Remove(name string) error { panic("TODO") }
  312. func (m memFS) ReadFile(name string) ([]byte, error) {
  313. v, ok := m[name]
  314. if !ok {
  315. return nil, fs.ErrNotExist
  316. }
  317. if s, ok := v.(string); ok {
  318. return []byte(s), nil
  319. }
  320. panic("TODO")
  321. }
  322. func (m memFS) Truncate(name string) error {
  323. v, ok := m[name]
  324. if !ok {
  325. return fs.ErrNotExist
  326. }
  327. if s, ok := v.(string); ok {
  328. m[name] = s[:0]
  329. }
  330. return nil
  331. }
  332. func (m memFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
  333. m[name] = string(contents)
  334. return nil
  335. }
  336. type dbusService struct {
  337. name, path string
  338. hook func() // if non-nil, run on ping
  339. }
  340. type dbusProperty struct {
  341. name, path string
  342. iface, member string
  343. hook func() (string, error) // what to return
  344. }
  345. type envBuilder struct {
  346. fs memFS
  347. dbus []dbusService
  348. dbusProperties []dbusProperty
  349. nmUsingResolved bool
  350. nmVersion string
  351. resolvconfStyle string
  352. }
  353. type envOption interface {
  354. apply(*envBuilder)
  355. }
  356. type envOpt func(*envBuilder)
  357. func (e envOpt) apply(b *envBuilder) {
  358. e(b)
  359. }
  360. func env(opts ...envOption) newOSConfigEnv {
  361. b := &envBuilder{
  362. fs: memFS{},
  363. }
  364. for _, opt := range opts {
  365. opt.apply(b)
  366. }
  367. return newOSConfigEnv{
  368. fs: b.fs,
  369. dbusPing: func(name, path string) error {
  370. for _, svc := range b.dbus {
  371. if svc.name == name && svc.path == path {
  372. if svc.hook != nil {
  373. svc.hook()
  374. }
  375. return nil
  376. }
  377. }
  378. return errors.New("dbus service not found")
  379. },
  380. dbusReadString: func(name, path, iface, member string) (string, error) {
  381. for _, svc := range b.dbusProperties {
  382. if svc.name == name && svc.path == path && svc.iface == iface && svc.member == member {
  383. return svc.hook()
  384. }
  385. }
  386. return "", errors.New("dbus property not found")
  387. },
  388. nmIsUsingResolved: func() error {
  389. if !b.nmUsingResolved {
  390. return errors.New("networkmanager not using resolved")
  391. }
  392. return nil
  393. },
  394. nmVersionBetween: func(first, last string) (bool, error) {
  395. outside := cmpver.Compare(b.nmVersion, first) < 0 || cmpver.Compare(b.nmVersion, last) > 0
  396. return !outside, nil
  397. },
  398. resolvconfStyle: func() string { return b.resolvconfStyle },
  399. }
  400. }
  401. func resolvDotConf(ss ...string) envOption {
  402. return envOpt(func(b *envBuilder) {
  403. b.fs["/etc/resolv.conf"] = strings.Join(ss, "\n")
  404. })
  405. }
  406. func nsswitchDotConf(ss ...string) envOption {
  407. return envOpt(func(b *envBuilder) {
  408. b.fs["/etc/nsswitch.conf"] = strings.Join(ss, "\n")
  409. })
  410. }
  411. // resolvedRunning returns an option that makes resolved reply to a dbusPing
  412. // and the ResolvConfMode property.
  413. func resolvedRunning() envOption {
  414. return resolvedStartOnPingAndThen(resolvedDbusProperty())
  415. }
  416. // resolvedDbusProperty returns an option that responds to the ResolvConfMode
  417. // property that resolved exposes.
  418. func resolvedDbusProperty() envOption {
  419. return setDbusProperty("org.freedesktop.resolve1", "/org/freedesktop/resolve1", "org.freedesktop.resolve1.Manager", "ResolvConfMode", "fortests")
  420. }
  421. // resolvedStartOnPingAndThen returns an option that makes resolved be
  422. // active but not yet running. On a dbus ping, it then applies the
  423. // provided options.
  424. func resolvedStartOnPingAndThen(opts ...envOption) envOption {
  425. return envOpt(func(b *envBuilder) {
  426. b.dbus = append(b.dbus, dbusService{
  427. name: "org.freedesktop.resolve1",
  428. path: "/org/freedesktop/resolve1",
  429. hook: func() {
  430. for _, opt := range opts {
  431. opt.apply(b)
  432. }
  433. },
  434. })
  435. })
  436. }
  437. func nmRunning(version string, usingResolved bool) envOption {
  438. return envOpt(func(b *envBuilder) {
  439. b.nmUsingResolved = usingResolved
  440. b.nmVersion = version
  441. b.dbus = append(b.dbus, dbusService{name: "org.freedesktop.NetworkManager", path: "/org/freedesktop/NetworkManager/DnsManager"})
  442. })
  443. }
  444. func resolvconf(s string) envOption {
  445. return envOpt(func(b *envBuilder) {
  446. b.resolvconfStyle = s
  447. })
  448. }
  449. func setDbusProperty(name, path, iface, member, value string) envOption {
  450. return envOpt(func(b *envBuilder) {
  451. b.dbusProperties = append(b.dbusProperties, dbusProperty{
  452. name: name,
  453. path: path,
  454. iface: iface,
  455. member: member,
  456. hook: func() (string, error) {
  457. return value, nil
  458. },
  459. })
  460. })
  461. }