main.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. //go:build linux
  4. // The containerboot binary is a wrapper for starting tailscaled in a container.
  5. // It handles reading the desired mode of operation out of environment
  6. // variables, bringing up and authenticating Tailscale, and any other
  7. // kubernetes-specific side jobs.
  8. //
  9. // As with most container things, configuration is passed through environment
  10. // variables. All configuration is optional.
  11. //
  12. // - TS_AUTHKEY: the authkey to use for login.
  13. // - TS_HOSTNAME: the hostname to request for the node.
  14. // - TS_ROUTES: subnet routes to advertise.
  15. // - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
  16. // destination.
  17. // - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
  18. // - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
  19. // - TS_USERSPACE: run with userspace networking (the default)
  20. // instead of kernel networking.
  21. // - TS_STATE_DIR: the directory in which to store tailscaled
  22. // state. The data should persist across container
  23. // restarts.
  24. // - TS_ACCEPT_DNS: whether to use the tailnet's DNS configuration.
  25. // - TS_KUBE_SECRET: the name of the Kubernetes secret in which to
  26. // store tailscaled state.
  27. // - TS_SOCKS5_SERVER: the address on which to listen for SOCKS5
  28. // proxying into the tailnet.
  29. // - TS_OUTBOUND_HTTP_PROXY_LISTEN: the address on which to listen
  30. // for HTTP proxying into the tailnet.
  31. // - TS_SOCKET: the path where the tailscaled LocalAPI socket should
  32. // be created.
  33. // - TS_AUTH_ONCE: if true, only attempt to log in if not already
  34. // logged in. If false (the default, for backwards
  35. // compatibility), forcibly log in every time the
  36. // container starts.
  37. //
  38. // When running on Kubernetes, containerboot defaults to storing state in the
  39. // "tailscale" kube secret. To store state on local disk instead, set
  40. // TS_KUBE_SECRET="" and TS_STATE_DIR=/path/to/storage/dir. The state dir should
  41. // be persistent storage.
  42. //
  43. // Additionally, if TS_AUTHKEY is not set and the TS_KUBE_SECRET contains an
  44. // "authkey" field, that key is used as the tailscale authkey.
  45. package main
  46. import (
  47. "context"
  48. "errors"
  49. "fmt"
  50. "io/fs"
  51. "log"
  52. "net/netip"
  53. "os"
  54. "os/exec"
  55. "os/signal"
  56. "path/filepath"
  57. "strconv"
  58. "strings"
  59. "syscall"
  60. "time"
  61. "golang.org/x/sys/unix"
  62. "tailscale.com/client/tailscale"
  63. "tailscale.com/ipn"
  64. "tailscale.com/util/deephash"
  65. )
  66. func main() {
  67. log.SetPrefix("boot: ")
  68. tailscale.I_Acknowledge_This_API_Is_Unstable = true
  69. cfg := &settings{
  70. AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
  71. Hostname: defaultEnv("TS_HOSTNAME", ""),
  72. Routes: defaultEnv("TS_ROUTES", ""),
  73. ProxyTo: defaultEnv("TS_DEST_IP", ""),
  74. DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
  75. ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
  76. InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
  77. UserspaceMode: defaultBool("TS_USERSPACE", true),
  78. StateDir: defaultEnv("TS_STATE_DIR", ""),
  79. AcceptDNS: defaultBool("TS_ACCEPT_DNS", false),
  80. KubeSecret: defaultEnv("TS_KUBE_SECRET", "tailscale"),
  81. SOCKSProxyAddr: defaultEnv("TS_SOCKS5_SERVER", ""),
  82. HTTPProxyAddr: defaultEnv("TS_OUTBOUND_HTTP_PROXY_LISTEN", ""),
  83. Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
  84. AuthOnce: defaultBool("TS_AUTH_ONCE", false),
  85. Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
  86. }
  87. if cfg.ProxyTo != "" && cfg.UserspaceMode {
  88. log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE")
  89. }
  90. if !cfg.UserspaceMode {
  91. if err := ensureTunFile(cfg.Root); err != nil {
  92. log.Fatalf("Unable to create tuntap device file: %v", err)
  93. }
  94. if cfg.ProxyTo != "" || cfg.Routes != "" {
  95. if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.Routes); err != nil {
  96. log.Printf("Failed to enable IP forwarding: %v", err)
  97. log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.")
  98. if cfg.InKubernetes {
  99. log.Fatalf("You can either set the sysctls as a privileged initContainer, or run the tailscale container with privileged=true.")
  100. } else {
  101. log.Fatalf("You can fix this by running the container with privileged=true, or the equivalent in your container runtime that permits access to sysctls.")
  102. }
  103. }
  104. }
  105. }
  106. if cfg.InKubernetes {
  107. initKube(cfg.Root)
  108. }
  109. // Context is used for all setup stuff until we're in steady
  110. // state, so that if something is hanging we eventually time out
  111. // and crashloop the container.
  112. ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
  113. defer cancel()
  114. if cfg.InKubernetes && cfg.KubeSecret != "" {
  115. canPatch, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
  116. if err != nil {
  117. log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
  118. }
  119. cfg.KubernetesCanPatch = canPatch
  120. if cfg.AuthKey == "" {
  121. key, err := findKeyInKubeSecret(ctx, cfg.KubeSecret)
  122. if err != nil {
  123. log.Fatalf("Getting authkey from kube secret: %v", err)
  124. }
  125. if key != "" {
  126. // This behavior of pulling authkeys from kube secrets was added
  127. // at the same time as the patch permission, so we can enforce
  128. // that we must be able to patch out the authkey after
  129. // authenticating if you want to use this feature. This avoids
  130. // us having to deal with the case where we might leave behind
  131. // an unnecessary reusable authkey in a secret, like a rake in
  132. // the grass.
  133. if !cfg.KubernetesCanPatch {
  134. log.Fatalf("authkey found in TS_KUBE_SECRET, but the pod doesn't have patch permissions on the secret to manage the authkey.")
  135. }
  136. log.Print("Using authkey found in kube secret")
  137. cfg.AuthKey = key
  138. } else {
  139. log.Print("No authkey found in kube secret and TS_AUTHKEY not provided, login will be interactive if needed.")
  140. }
  141. }
  142. }
  143. client, daemonPid, err := startTailscaled(ctx, cfg)
  144. if err != nil {
  145. log.Fatalf("failed to bring up tailscale: %v", err)
  146. }
  147. w, err := client.WatchIPNBus(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialPrefs|ipn.NotifyInitialState)
  148. if err != nil {
  149. log.Fatalf("failed to watch tailscaled for updates: %v", err)
  150. }
  151. // Because we're still shelling out to `tailscale up` to get access to its
  152. // flag parser, we have to stop watching the IPN bus so that we can block on
  153. // the subcommand without stalling anything. Then once it's done, we resume
  154. // watching the bus.
  155. //
  156. // Depending on the requested mode of operation, this auth step happens at
  157. // different points in containerboot's lifecycle, hence the helper function.
  158. didLogin := false
  159. authTailscale := func() error {
  160. if didLogin {
  161. return nil
  162. }
  163. didLogin = true
  164. w.Close()
  165. if err := tailscaleUp(ctx, cfg); err != nil {
  166. return fmt.Errorf("failed to auth tailscale: %v", err)
  167. }
  168. w, err = client.WatchIPNBus(ctx, ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
  169. if err != nil {
  170. return fmt.Errorf("rewatching tailscaled for updates after auth: %v", err)
  171. }
  172. return nil
  173. }
  174. if !cfg.AuthOnce {
  175. if err := authTailscale(); err != nil {
  176. log.Fatalf("failed to auth tailscale: %v", err)
  177. }
  178. }
  179. authLoop:
  180. for {
  181. n, err := w.Next()
  182. if err != nil {
  183. log.Fatalf("failed to read from tailscaled: %v", err)
  184. }
  185. if n.State != nil {
  186. switch *n.State {
  187. case ipn.NeedsLogin:
  188. if err := authTailscale(); err != nil {
  189. log.Fatalf("failed to auth tailscale: %v", err)
  190. }
  191. case ipn.NeedsMachineAuth:
  192. log.Printf("machine authorization required, please visit the admin panel")
  193. case ipn.Running:
  194. // Technically, all we want is to keep monitoring the bus for
  195. // netmap updates. However, in order to make the container crash
  196. // if tailscale doesn't initially come up, the watch has a
  197. // startup deadline on it. So, we have to break out of this
  198. // watch loop, cancel the watch, and watch again with no
  199. // deadline to continue monitoring for changes.
  200. break authLoop
  201. default:
  202. log.Printf("tailscaled in state %q, waiting", *n.State)
  203. }
  204. }
  205. }
  206. w.Close()
  207. if cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch && cfg.AuthOnce {
  208. // We were told to only auth once, so any secret-bound
  209. // authkey is no longer needed. We don't strictly need to
  210. // wipe it, but it's good hygiene.
  211. log.Printf("Deleting authkey from kube secret")
  212. if err := deleteAuthKey(ctx, cfg.KubeSecret); err != nil {
  213. log.Fatalf("deleting authkey from kube secret: %v", err)
  214. }
  215. }
  216. w, err = client.WatchIPNBus(context.Background(), ipn.NotifyInitialNetMap|ipn.NotifyInitialState)
  217. if err != nil {
  218. log.Fatalf("rewatching tailscaled for updates after auth: %v", err)
  219. }
  220. var (
  221. wantProxy = cfg.ProxyTo != ""
  222. wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch
  223. startupTasksDone = false
  224. currentIPs deephash.Sum // tailscale IPs assigned to device
  225. currentDeviceInfo deephash.Sum // device ID and fqdn
  226. )
  227. for {
  228. n, err := w.Next()
  229. if err != nil {
  230. log.Fatalf("failed to read from tailscaled: %v", err)
  231. }
  232. if n.State != nil && *n.State != ipn.Running {
  233. // Something's gone wrong and we've left the authenticated state.
  234. // Our container image never recovered gracefully from this, and the
  235. // control flow required to make it work now is hard. So, just crash
  236. // the container and rely on the container runtime to restart us,
  237. // whereupon we'll go through initial auth again.
  238. log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State)
  239. }
  240. if n.NetMap != nil {
  241. if cfg.ProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(&currentIPs, &n.NetMap.Addresses) {
  242. if err := installIPTablesRule(ctx, cfg.ProxyTo, n.NetMap.Addresses); err != nil {
  243. log.Fatalf("installing proxy rules: %v", err)
  244. }
  245. }
  246. deviceInfo := []any{n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name}
  247. if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(&currentDeviceInfo, &deviceInfo) {
  248. if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name); err != nil {
  249. log.Fatalf("storing device ID in kube secret: %v", err)
  250. }
  251. }
  252. }
  253. if !startupTasksDone {
  254. if (!wantProxy || currentIPs != deephash.Sum{}) && (!wantDeviceInfo || currentDeviceInfo != deephash.Sum{}) {
  255. // This log message is used in tests to detect when all
  256. // post-auth configuration is done.
  257. log.Println("Startup complete, waiting for shutdown signal")
  258. startupTasksDone = true
  259. // Reap all processes, since we are PID1 and need to collect zombies. We can
  260. // only start doing this once we've stopped shelling out to things
  261. // `tailscale up`, otherwise this goroutine can reap the CLI subprocesses
  262. // and wedge bringup.
  263. go func() {
  264. for {
  265. var status unix.WaitStatus
  266. pid, err := unix.Wait4(-1, &status, 0, nil)
  267. if errors.Is(err, unix.EINTR) {
  268. continue
  269. }
  270. if err != nil {
  271. log.Fatalf("Waiting for exited processes: %v", err)
  272. }
  273. if pid == daemonPid {
  274. log.Printf("Tailscaled exited")
  275. os.Exit(0)
  276. }
  277. }
  278. }()
  279. }
  280. }
  281. }
  282. }
  283. func startTailscaled(ctx context.Context, cfg *settings) (*tailscale.LocalClient, int, error) {
  284. args := tailscaledArgs(cfg)
  285. sigCh := make(chan os.Signal, 1)
  286. signal.Notify(sigCh, unix.SIGTERM, unix.SIGINT)
  287. // tailscaled runs without context, since it needs to persist
  288. // beyond the startup timeout in ctx.
  289. cmd := exec.Command("tailscaled", args...)
  290. cmd.Stdout = os.Stdout
  291. cmd.Stderr = os.Stderr
  292. cmd.SysProcAttr = &syscall.SysProcAttr{
  293. Setpgid: true,
  294. }
  295. log.Printf("Starting tailscaled")
  296. if err := cmd.Start(); err != nil {
  297. return nil, 0, fmt.Errorf("starting tailscaled failed: %v", err)
  298. }
  299. go func() {
  300. <-sigCh
  301. log.Printf("Received SIGTERM from container runtime, shutting down tailscaled")
  302. cmd.Process.Signal(unix.SIGTERM)
  303. }()
  304. // Wait for the socket file to appear, otherwise API ops will racily fail.
  305. log.Printf("Waiting for tailscaled socket")
  306. for {
  307. if ctx.Err() != nil {
  308. log.Fatalf("Timed out waiting for tailscaled socket")
  309. }
  310. _, err := os.Stat(cfg.Socket)
  311. if errors.Is(err, fs.ErrNotExist) {
  312. time.Sleep(100 * time.Millisecond)
  313. continue
  314. } else if err != nil {
  315. log.Fatalf("Waiting for tailscaled socket: %v", err)
  316. }
  317. break
  318. }
  319. tsClient := &tailscale.LocalClient{
  320. Socket: cfg.Socket,
  321. UseSocketOnly: true,
  322. }
  323. return tsClient, cmd.Process.Pid, nil
  324. }
  325. // tailscaledArgs uses cfg to construct the argv for tailscaled.
  326. func tailscaledArgs(cfg *settings) []string {
  327. args := []string{"--socket=" + cfg.Socket}
  328. switch {
  329. case cfg.InKubernetes && cfg.KubeSecret != "":
  330. args = append(args, "--state=kube:"+cfg.KubeSecret)
  331. if cfg.StateDir == "" {
  332. cfg.StateDir = "/tmp"
  333. }
  334. fallthrough
  335. case cfg.StateDir != "":
  336. args = append(args, "--statedir="+cfg.StateDir)
  337. default:
  338. args = append(args, "--state=mem:", "--statedir=/tmp")
  339. }
  340. if cfg.UserspaceMode {
  341. args = append(args, "--tun=userspace-networking")
  342. } else if err := ensureTunFile(cfg.Root); err != nil {
  343. log.Fatalf("ensuring that /dev/net/tun exists: %v", err)
  344. }
  345. if cfg.SOCKSProxyAddr != "" {
  346. args = append(args, "--socks5-server="+cfg.SOCKSProxyAddr)
  347. }
  348. if cfg.HTTPProxyAddr != "" {
  349. args = append(args, "--outbound-http-proxy-listen="+cfg.HTTPProxyAddr)
  350. }
  351. if cfg.DaemonExtraArgs != "" {
  352. args = append(args, strings.Fields(cfg.DaemonExtraArgs)...)
  353. }
  354. return args
  355. }
  356. // tailscaleUp uses cfg to run 'tailscale up'.
  357. func tailscaleUp(ctx context.Context, cfg *settings) error {
  358. args := []string{"--socket=" + cfg.Socket, "up"}
  359. if cfg.AcceptDNS {
  360. args = append(args, "--accept-dns=true")
  361. } else {
  362. args = append(args, "--accept-dns=false")
  363. }
  364. if cfg.AuthKey != "" {
  365. args = append(args, "--authkey="+cfg.AuthKey)
  366. }
  367. if cfg.Routes != "" {
  368. args = append(args, "--advertise-routes="+cfg.Routes)
  369. }
  370. if cfg.Hostname != "" {
  371. args = append(args, "--hostname="+cfg.Hostname)
  372. }
  373. if cfg.ExtraArgs != "" {
  374. args = append(args, strings.Fields(cfg.ExtraArgs)...)
  375. }
  376. log.Printf("Running 'tailscale up'")
  377. cmd := exec.CommandContext(ctx, "tailscale", args...)
  378. cmd.Stdout = os.Stdout
  379. cmd.Stderr = os.Stderr
  380. if err := cmd.Run(); err != nil {
  381. return fmt.Errorf("tailscale up failed: %v", err)
  382. }
  383. return nil
  384. }
  385. // ensureTunFile checks that /dev/net/tun exists, creating it if
  386. // missing.
  387. func ensureTunFile(root string) error {
  388. // Verify that /dev/net/tun exists, in some container envs it
  389. // needs to be mknod-ed.
  390. if _, err := os.Stat(filepath.Join(root, "dev/net")); errors.Is(err, fs.ErrNotExist) {
  391. if err := os.MkdirAll(filepath.Join(root, "dev/net"), 0755); err != nil {
  392. return err
  393. }
  394. }
  395. if _, err := os.Stat(filepath.Join(root, "dev/net/tun")); errors.Is(err, fs.ErrNotExist) {
  396. dev := unix.Mkdev(10, 200) // tuntap major and minor
  397. if err := unix.Mknod(filepath.Join(root, "dev/net/tun"), 0600|unix.S_IFCHR, int(dev)); err != nil {
  398. return err
  399. }
  400. }
  401. return nil
  402. }
  403. // ensureIPForwarding enables IPv4/IPv6 forwarding for the container.
  404. func ensureIPForwarding(root, proxyTo, routes string) error {
  405. var (
  406. v4Forwarding, v6Forwarding bool
  407. )
  408. if proxyTo != "" {
  409. proxyIP, err := netip.ParseAddr(proxyTo)
  410. if err != nil {
  411. return fmt.Errorf("invalid proxy destination IP: %v", err)
  412. }
  413. if proxyIP.Is4() {
  414. v4Forwarding = true
  415. } else {
  416. v6Forwarding = true
  417. }
  418. }
  419. if routes != "" {
  420. for _, route := range strings.Split(routes, ",") {
  421. cidr, err := netip.ParsePrefix(route)
  422. if err != nil {
  423. return fmt.Errorf("invalid subnet route: %v", err)
  424. }
  425. if cidr.Addr().Is4() {
  426. v4Forwarding = true
  427. } else {
  428. v6Forwarding = true
  429. }
  430. }
  431. }
  432. var paths []string
  433. if v4Forwarding {
  434. paths = append(paths, filepath.Join(root, "proc/sys/net/ipv4/ip_forward"))
  435. }
  436. if v6Forwarding {
  437. paths = append(paths, filepath.Join(root, "proc/sys/net/ipv6/conf/all/forwarding"))
  438. }
  439. // In some common configurations (e.g. default docker,
  440. // kubernetes), the container environment denies write access to
  441. // most sysctls, including IP forwarding controls. Check the
  442. // sysctl values before trying to change them, so that we
  443. // gracefully do nothing if the container's already been set up
  444. // properly by e.g. a k8s initContainer.
  445. for _, path := range paths {
  446. bs, err := os.ReadFile(path)
  447. if err != nil {
  448. return fmt.Errorf("reading %q: %w", path, err)
  449. }
  450. if v := strings.TrimSpace(string(bs)); v != "1" {
  451. if err := os.WriteFile(path, []byte("1"), 0644); err != nil {
  452. return fmt.Errorf("enabling %q: %w", path, err)
  453. }
  454. }
  455. }
  456. return nil
  457. }
  458. func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
  459. dst, err := netip.ParseAddr(dstStr)
  460. if err != nil {
  461. return err
  462. }
  463. argv0 := "iptables"
  464. if dst.Is6() {
  465. argv0 = "ip6tables"
  466. }
  467. var local string
  468. for _, pfx := range tsIPs {
  469. if !pfx.IsSingleIP() {
  470. continue
  471. }
  472. if pfx.Addr().Is4() != dst.Is4() {
  473. continue
  474. }
  475. local = pfx.Addr().String()
  476. break
  477. }
  478. if local == "" {
  479. return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
  480. }
  481. // Technically, if the control server ever changes the IPs assigned to this
  482. // node, we'll slowly accumulate iptables rules. This shouldn't happen, so
  483. // for now we'll live with it.
  484. cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "-d", local, "-j", "DNAT", "--to-destination", dstStr)
  485. cmd.Stdout = os.Stdout
  486. cmd.Stderr = os.Stderr
  487. if err := cmd.Run(); err != nil {
  488. return fmt.Errorf("executing iptables failed: %w", err)
  489. }
  490. return nil
  491. }
  492. // settings is all the configuration for containerboot.
  493. type settings struct {
  494. AuthKey string
  495. Hostname string
  496. Routes string
  497. ProxyTo string
  498. DaemonExtraArgs string
  499. ExtraArgs string
  500. InKubernetes bool
  501. UserspaceMode bool
  502. StateDir string
  503. AcceptDNS bool
  504. KubeSecret string
  505. SOCKSProxyAddr string
  506. HTTPProxyAddr string
  507. Socket string
  508. AuthOnce bool
  509. Root string
  510. KubernetesCanPatch bool
  511. }
  512. // defaultEnv returns the value of the given envvar name, or defVal if
  513. // unset.
  514. func defaultEnv(name, defVal string) string {
  515. if v, ok := os.LookupEnv(name); ok {
  516. return v
  517. }
  518. return defVal
  519. }
  520. func defaultEnvs(names []string, defVal string) string {
  521. for _, name := range names {
  522. if v, ok := os.LookupEnv(name); ok {
  523. return v
  524. }
  525. }
  526. return defVal
  527. }
  528. // defaultBool returns the boolean value of the given envvar name, or
  529. // defVal if unset or not a bool.
  530. func defaultBool(name string, defVal bool) bool {
  531. v := os.Getenv(name)
  532. ret, err := strconv.ParseBool(v)
  533. if err != nil {
  534. return defVal
  535. }
  536. return ret
  537. }