mirror of
				https://github.com/juanfont/headscale.git
				synced 2025-10-28 10:51:44 +01:00 
			
		
		
		
	Redo DNS configuration (#2034)
this commit changes and streamlines the dns_config into a new key, dns. It removes a combination of outdates and incompatible configuration options that made it easy to confuse what headscale could and could not do, or what to expect from ones configuration. Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
		
							parent
							
								
									022fb24cd9
								
							
						
					
					
						commit
						ac8491efec
					
				
							
								
								
									
										3
									
								
								.github/workflows/test-integration.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.github/workflows/test-integration.yaml
									
									
									
									
										vendored
									
									
								
							| @ -37,6 +37,8 @@ jobs: | ||||
|           - TestNodeRenameCommand | ||||
|           - TestNodeMoveCommand | ||||
|           - TestPolicyCommand | ||||
|           - TestResolveMagicDNS | ||||
|           - TestValidateResolvConf | ||||
|           - TestDERPServerScenario | ||||
|           - TestPingAllByIP | ||||
|           - TestPingAllByIPPublicDERP | ||||
| @ -45,7 +47,6 @@ jobs: | ||||
|           - TestEphemeral2006DeletedTooQuickly | ||||
|           - TestPingAllByHostname | ||||
|           - TestTaildrop | ||||
|           - TestResolveMagicDNS | ||||
|           - TestExpireNode | ||||
|           - TestNodeOnlineStatus | ||||
|           - TestPingAllByIPManyUpDown | ||||
|  | ||||
| @ -29,7 +29,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ | ||||
|   - Adds additional configuration for PostgreSQL for setting max open, idle connection and idle connection lifetime. | ||||
| - API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553) | ||||
| - Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611) | ||||
|   - The latest supported client is 1.38 | ||||
|   - The latest supported client is 1.42 | ||||
| - Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564) | ||||
|   - If no DERP is configured, the server will fail to start, this can be because it cannot load the DERPMap from file or url. | ||||
| - Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611) | ||||
| @ -43,9 +43,12 @@ after improving the test harness as part of adopting [#1460](https://github.com/ | ||||
| - MagicDNS domains no longer contain usernames []() | ||||
|   - This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information. | ||||
|   - `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed. | ||||
|   - This option brings Headscales behaviour in line with Tailscale. | ||||
|     - dns.base_domain can no longer be the same as (or part of) server_url. | ||||
|     - This option brings Headscales behaviour in line with Tailscale. | ||||
| - YAML files are no longer supported for headscale policy.  [#1792](https://github.com/juanfont/headscale/pull/1792) | ||||
|   - HuJSON is now the only supported format for policy. | ||||
| - DNS configuration has been restructured [#2034](https://github.com/juanfont/headscale/pull/2034) | ||||
|   - Please review the new [config-example.yaml](./config-example.yaml) for the new structure. | ||||
| 
 | ||||
| ### Changes | ||||
| 
 | ||||
|  | ||||
| @ -63,7 +63,6 @@ func (*Suite) TestConfigFileLoading(c *check.C) { | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") | ||||
| 	c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") | ||||
| 	c.Assert( | ||||
| 		util.GetFileMode("unix_socket_permission"), | ||||
| 		check.Equals, | ||||
| @ -106,7 +105,6 @@ func (*Suite) TestConfigLoading(c *check.C) { | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") | ||||
| 	c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") | ||||
| 	c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") | ||||
| 	c.Assert( | ||||
| 		util.GetFileMode("unix_socket_permission"), | ||||
| 		check.Equals, | ||||
| @ -116,39 +114,6 @@ func (*Suite) TestConfigLoading(c *check.C) { | ||||
| 	c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false) | ||||
| } | ||||
| 
 | ||||
| func (*Suite) TestDNSConfigLoading(c *check.C) { | ||||
| 	tmpDir, err := os.MkdirTemp("", "headscale") | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 	defer os.RemoveAll(tmpDir) | ||||
| 
 | ||||
| 	path, err := os.Getwd() | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Symlink the example config file
 | ||||
| 	err = os.Symlink( | ||||
| 		filepath.Clean(path+"/../../config-example.yaml"), | ||||
| 		filepath.Join(tmpDir, "config.yaml"), | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		c.Fatal(err) | ||||
| 	} | ||||
| 
 | ||||
| 	// Load example config, it should load without validation errors
 | ||||
| 	err = types.LoadConfig(tmpDir, false) | ||||
| 	c.Assert(err, check.IsNil) | ||||
| 
 | ||||
| 	dnsConfig, baseDomain := types.GetDNSConfig() | ||||
| 
 | ||||
| 	c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1") | ||||
| 	c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1") | ||||
| 	c.Assert(dnsConfig.Proxied, check.Equals, true) | ||||
| 	c.Assert(baseDomain, check.Equals, "example.com") | ||||
| } | ||||
| 
 | ||||
| func writeConfig(c *check.C, tmpDir string, configYaml []byte) { | ||||
| 	// Populate a custom config file
 | ||||
| 	configFile := filepath.Join(tmpDir, "config.yaml") | ||||
|  | ||||
| @ -224,43 +224,60 @@ policy: | ||||
| # - https://tailscale.com/kb/1081/magicdns/ | ||||
| # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ | ||||
| # | ||||
| dns_config: | ||||
|   # Whether to prefer using Headscale provided DNS or use local. | ||||
|   override_local_dns: true | ||||
| # Please not that for the DNS configuration to have any effect, | ||||
| # clients must have the `--accept-ds=true` option enabled. This is the | ||||
| # default for the Tailscale client. This option is enabled by default | ||||
| # in the Tailscale client. | ||||
| # | ||||
| # Setting _any_ of the configuration and `--accept-dns=true` on the | ||||
| # clients will integrate with the DNS manager on the client or | ||||
| # overwrite /etc/resolv.conf. | ||||
| # https://tailscale.com/kb/1235/resolv-conf | ||||
| # | ||||
| # If you want stop Headscale from managing the DNS configuration | ||||
| # all the fields under `dns` should be set to empty values. | ||||
| dns: | ||||
|   # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). | ||||
|   # Only works if there is at least a nameserver defined. | ||||
|   magic_dns: true | ||||
| 
 | ||||
|   # Defines the base domain to create the hostnames for MagicDNS. | ||||
|   # This domain _must_ be different from the server_url domain. | ||||
|   # `base_domain` must be a FQDN, without the trailing dot. | ||||
|   # The FQDN of the hosts will be | ||||
|   # `hostname.base_domain` (e.g., _myhost.example.com_). | ||||
|   base_domain: example.com | ||||
| 
 | ||||
|   # List of DNS servers to expose to clients. | ||||
|   nameservers: | ||||
|     - 1.1.1.1 | ||||
|     global: | ||||
|       - 1.1.1.1 | ||||
|       - 1.0.0.1 | ||||
|       - 2606:4700:4700::1111 | ||||
|       - 2606:4700:4700::1001 | ||||
| 
 | ||||
|   # NextDNS (see https://tailscale.com/kb/1218/nextdns/). | ||||
|   # "abc123" is example NextDNS ID, replace with yours. | ||||
|   # | ||||
|   # With metadata sharing: | ||||
|   # nameservers: | ||||
|   #   - https://dns.nextdns.io/abc123 | ||||
|   # | ||||
|   # Without metadata sharing: | ||||
|   # nameservers: | ||||
|   #   - 2a07:a8c0::ab:c123 | ||||
|   #   - 2a07:a8c1::ab:c123 | ||||
|       # NextDNS (see https://tailscale.com/kb/1218/nextdns/). | ||||
|       # "abc123" is example NextDNS ID, replace with yours. | ||||
|       # - https://dns.nextdns.io/abc123 | ||||
| 
 | ||||
|   # Split DNS (see https://tailscale.com/kb/1054/dns/), | ||||
|   # list of search domains and the DNS to query for each one. | ||||
|   # | ||||
|   # restricted_nameservers: | ||||
|   #   foo.bar.com: | ||||
|   #     - 1.1.1.1 | ||||
|   #   darp.headscale.net: | ||||
|   #     - 1.1.1.1 | ||||
|   #     - 8.8.8.8 | ||||
|     # Split DNS (see https://tailscale.com/kb/1054/dns/), | ||||
|     # a map of domains and which DNS server to use for each. | ||||
|     split: | ||||
|       {} | ||||
|       # foo.bar.com: | ||||
|       #   - 1.1.1.1 | ||||
|       # darp.headscale.net: | ||||
|       #   - 1.1.1.1 | ||||
|       #   - 8.8.8.8 | ||||
| 
 | ||||
|   # Search domains to inject. | ||||
|   domains: [] | ||||
|   # Set custom DNS search domains. With MagicDNS enabled, | ||||
|   # your tailnet base_domain is always the first search domain. | ||||
|   search_domains: [] | ||||
| 
 | ||||
|   # Extra DNS records | ||||
|   # so far only A-records are supported (on the tailscale side) | ||||
|   # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations | ||||
|   # extra_records: | ||||
|   extra_records: [] | ||||
|   #   - name: "grafana.myvpn.example.com" | ||||
|   #     type: "A" | ||||
|   #     value: "100.64.0.3" | ||||
| @ -268,10 +285,6 @@ dns_config: | ||||
|   #   # you can also put it in one line | ||||
|   #   - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } | ||||
| 
 | ||||
|   # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). | ||||
|   # Only works if there is at least a nameserver defined. | ||||
|   magic_dns: true | ||||
| 
 | ||||
|   # DEPRECATED | ||||
|   # Use the username as part of the DNS name for nodes, with this option enabled: | ||||
|   # node1.username.example.com | ||||
| @ -281,12 +294,6 @@ dns_config: | ||||
|   # while in upstream Tailscale, the username is not included. | ||||
|   use_username_in_magic_dns: false | ||||
| 
 | ||||
|   # Defines the base domain to create the hostnames for MagicDNS. | ||||
|   # `base_domain` must be a FQDNs, without the trailing dot. | ||||
|   # The FQDN of the hosts will be | ||||
|   # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_). | ||||
|   base_domain: example.com | ||||
| 
 | ||||
| # Unix socket used for the CLI to connect without authentication | ||||
| # Note: for production you will want to set this to something like: | ||||
| unix_socket: /var/run/headscale/headscale.sock | ||||
|  | ||||
| @ -31,7 +31,7 @@ | ||||
| 
 | ||||
|           # When updating go.mod or go.sum, a new sha will need to be calculated, | ||||
|           # update this if you have a mismatch after doing a change to thos files. | ||||
|           vendorHash = "sha256-EorT2AVwA3usly/LcNor6r5UIhLCdj3L4O4ilgTIC2o="; | ||||
|           vendorHash = "sha256-08N9ZdUM3Lw0ad89Vpy01e/qJQoMRPj8n4Jd7Aecgjw="; | ||||
| 
 | ||||
|           subPackages = ["cmd/headscale"]; | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										25
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								go.mod
									
									
									
									
									
								
							| @ -31,15 +31,15 @@ require ( | ||||
| 	github.com/samber/lo v1.39.0 | ||||
| 	github.com/sasha-s/go-deadlock v0.3.1 | ||||
| 	github.com/spf13/cobra v1.8.0 | ||||
| 	github.com/spf13/viper v1.18.2 | ||||
| 	github.com/spf13/viper v1.20.0-alpha.6 | ||||
| 	github.com/stretchr/testify v1.9.0 | ||||
| 	github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a | ||||
| 	github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 | ||||
| 	github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e | ||||
| 	go4.org/netipx v0.0.0-20231129151722-fdeea329fbba | ||||
| 	golang.org/x/crypto v0.23.0 | ||||
| 	golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 | ||||
| 	golang.org/x/net v0.25.0 | ||||
| 	golang.org/x/crypto v0.25.0 | ||||
| 	golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 | ||||
| 	golang.org/x/net v0.27.0 | ||||
| 	golang.org/x/oauth2 v0.20.0 | ||||
| 	golang.org/x/sync v0.7.0 | ||||
| 	google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 | ||||
| @ -101,6 +101,7 @@ require ( | ||||
| 	github.com/go-jose/go-jose/v4 v4.0.1 // indirect | ||||
| 	github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect | ||||
| 	github.com/go-ole/go-ole v1.3.0 // indirect | ||||
| 	github.com/go-viper/mapstructure/v2 v2.0.0 // indirect | ||||
| 	github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect | ||||
| 	github.com/gogo/protobuf v1.3.2 // indirect | ||||
| 	github.com/golang-jwt/jwt/v5 v5.2.1 // indirect | ||||
| @ -117,7 +118,6 @@ require ( | ||||
| 	github.com/gorilla/csrf v1.7.2 // indirect | ||||
| 	github.com/gorilla/securecookie v1.1.2 // indirect | ||||
| 	github.com/hashicorp/go-version v1.6.0 // indirect | ||||
| 	github.com/hashicorp/hcl v1.0.0 // indirect | ||||
| 	github.com/hdevalence/ed25519consensus v0.2.0 // indirect | ||||
| 	github.com/illarion/gonotify v1.0.1 // indirect | ||||
| 	github.com/inconshreveable/mousetrap v1.1.0 // indirect | ||||
| @ -137,7 +137,6 @@ require ( | ||||
| 	github.com/kr/text v0.2.0 // indirect | ||||
| 	github.com/lib/pq v1.10.7 // indirect | ||||
| 	github.com/lithammer/fuzzysearch v1.1.8 // indirect | ||||
| 	github.com/magiconair/properties v1.8.7 // indirect | ||||
| 	github.com/mattn/go-colorable v0.1.13 // indirect | ||||
| 	github.com/mattn/go-isatty v0.0.20 // indirect | ||||
| 	github.com/mattn/go-runewidth v0.0.15 // indirect | ||||
| @ -166,8 +165,7 @@ require ( | ||||
| 	github.com/rivo/uniseg v0.4.7 // indirect | ||||
| 	github.com/rogpeppe/go-internal v1.12.0 // indirect | ||||
| 	github.com/safchain/ethtool v0.3.0 // indirect | ||||
| 	github.com/sagikazarmark/locafero v0.4.0 // indirect | ||||
| 	github.com/sagikazarmark/slog-shim v0.1.0 // indirect | ||||
| 	github.com/sagikazarmark/locafero v0.6.0 // indirect | ||||
| 	github.com/sirupsen/logrus v1.9.3 // indirect | ||||
| 	github.com/sourcegraph/conc v0.3.0 // indirect | ||||
| 	github.com/spf13/afero v1.11.0 // indirect | ||||
| @ -195,16 +193,15 @@ require ( | ||||
| 	github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect | ||||
| 	go.uber.org/multierr v1.11.0 // indirect | ||||
| 	go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect | ||||
| 	golang.org/x/mod v0.17.0 // indirect | ||||
| 	golang.org/x/sys v0.20.0 // indirect | ||||
| 	golang.org/x/term v0.20.0 // indirect | ||||
| 	golang.org/x/text v0.15.0 // indirect | ||||
| 	golang.org/x/mod v0.19.0 // indirect | ||||
| 	golang.org/x/sys v0.22.0 // indirect | ||||
| 	golang.org/x/term v0.22.0 // indirect | ||||
| 	golang.org/x/text v0.16.0 // indirect | ||||
| 	golang.org/x/time v0.5.0 // indirect | ||||
| 	golang.org/x/tools v0.21.0 // indirect | ||||
| 	golang.org/x/tools v0.23.0 // indirect | ||||
| 	golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect | ||||
| 	golang.zx2c4.com/wireguard/windows v0.5.3 // indirect | ||||
| 	google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect | ||||
| 	gopkg.in/ini.v1 v1.67.0 // indirect | ||||
| 	gopkg.in/yaml.v2 v2.4.0 // indirect | ||||
| 	gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect | ||||
| 	modernc.org/libc v1.50.6 // indirect | ||||
|  | ||||
							
								
								
									
										51
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								go.sum
									
									
									
									
									
								
							| @ -180,6 +180,8 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW | ||||
| github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= | ||||
| github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= | ||||
| github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= | ||||
| github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= | ||||
| github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | ||||
| github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= | ||||
| github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= | ||||
| github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= | ||||
| @ -240,11 +242,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 | ||||
| github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= | ||||
| github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= | ||||
| github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= | ||||
| github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= | ||||
| github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= | ||||
| github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= | ||||
| github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= | ||||
| github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= | ||||
| github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= | ||||
| github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= | ||||
| github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= | ||||
| @ -312,8 +311,6 @@ github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= | ||||
| github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= | ||||
| github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= | ||||
| github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= | ||||
| github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= | ||||
| github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= | ||||
| github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= | ||||
| github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= | ||||
| github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= | ||||
| @ -419,10 +416,8 @@ github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR | ||||
| github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= | ||||
| github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= | ||||
| github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= | ||||
| github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= | ||||
| github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= | ||||
| github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= | ||||
| github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= | ||||
| github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= | ||||
| github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= | ||||
| github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= | ||||
| github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= | ||||
| github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= | ||||
| @ -443,8 +438,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= | ||||
| github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= | ||||
| github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= | ||||
| github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= | ||||
| github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= | ||||
| github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= | ||||
| github.com/spf13/viper v1.20.0-alpha.6 h1:f65Cr/+2qk4GfHC0xqT/isoupQppwN5+VLRztUGTDbY= | ||||
| github.com/spf13/viper v1.20.0-alpha.6/go.mod h1:CGBZzv0c9fOUASm6rfus4wdeIjR/04NOLq1P4KRhX3k= | ||||
| github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||||
| github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= | ||||
| @ -538,11 +533,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U | ||||
| golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= | ||||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | ||||
| golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= | ||||
| golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= | ||||
| golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= | ||||
| golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= | ||||
| golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= | ||||
| golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= | ||||
| golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= | ||||
| golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= | ||||
| golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= | ||||
| golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= | ||||
| golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= | ||||
| golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= | ||||
| golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= | ||||
| @ -555,8 +550,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= | ||||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | ||||
| golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||||
| golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= | ||||
| golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||
| golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= | ||||
| golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= | ||||
| golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= | ||||
| @ -569,8 +564,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v | ||||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | ||||
| golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | ||||
| golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||||
| golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= | ||||
| golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= | ||||
| golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= | ||||
| golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= | ||||
| golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= | ||||
| golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= | ||||
| golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= | ||||
| @ -615,8 +610,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | ||||
| golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= | ||||
| golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= | ||||
| golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= | ||||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | ||||
| golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | ||||
| @ -624,8 +619,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX | ||||
| golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | ||||
| golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= | ||||
| golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= | ||||
| golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= | ||||
| golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= | ||||
| golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= | ||||
| golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= | ||||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | ||||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | ||||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | ||||
| @ -633,8 +628,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | ||||
| golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= | ||||
| golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||
| golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= | ||||
| golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= | ||||
| golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= | ||||
| golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= | ||||
| golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= | ||||
| golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= | ||||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | ||||
| @ -648,8 +643,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY | ||||
| golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= | ||||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | ||||
| golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | ||||
| golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= | ||||
| golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= | ||||
| golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= | ||||
| golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= | ||||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | ||||
| @ -681,8 +676,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 | ||||
| gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= | ||||
| gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= | ||||
| gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= | ||||
| gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= | ||||
| gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
| gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
|  | ||||
| @ -36,8 +36,7 @@ func tailNodes( | ||||
| 	return tNodes, nil | ||||
| } | ||||
| 
 | ||||
| // tailNode converts a Node into a Tailscale Node. includeRoutes is false for shared nodes
 | ||||
| // as per the expected behaviour in the official SaaS.
 | ||||
| // tailNode converts a Node into a Tailscale Node.
 | ||||
| func tailNode( | ||||
| 	node *types.Node, | ||||
| 	capVer tailcfg.CapabilityVersion, | ||||
|  | ||||
| @ -55,12 +55,14 @@ func TestTailNode(t *testing.T) { | ||||
| 		{ | ||||
| 			name: "empty-node", | ||||
| 			node: &types.Node{ | ||||
| 				Hostinfo: &tailcfg.Hostinfo{}, | ||||
| 				GivenName: "empty", | ||||
| 				Hostinfo:  &tailcfg.Hostinfo{}, | ||||
| 			}, | ||||
| 			pol:        &policy.ACLPolicy{}, | ||||
| 			dnsConfig:  &tailcfg.DNSConfig{}, | ||||
| 			baseDomain: "", | ||||
| 			want: &tailcfg.Node{ | ||||
| 				Name:              "empty", | ||||
| 				StableID:          "0", | ||||
| 				Addresses:         []netip.Prefix{}, | ||||
| 				AllowedIPs:        []netip.Prefix{}, | ||||
|  | ||||
| @ -166,7 +166,7 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error { | ||||
| } | ||||
| 
 | ||||
| const ( | ||||
| 	MinimumCapVersion tailcfg.CapabilityVersion = 58 | ||||
| 	MinimumCapVersion tailcfg.CapabilityVersion = 61 | ||||
| ) | ||||
| 
 | ||||
| // NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
 | ||||
|  | ||||
| @ -20,6 +20,7 @@ import ( | ||||
| 	"tailscale.com/net/tsaddr" | ||||
| 	"tailscale.com/tailcfg" | ||||
| 	"tailscale.com/types/dnstype" | ||||
| 	"tailscale.com/util/set" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| @ -88,6 +89,20 @@ type Config struct { | ||||
| 	Tuning Tuning | ||||
| } | ||||
| 
 | ||||
| type DNSConfig struct { | ||||
| 	MagicDNS           bool   `mapstructure:"magic_dns"` | ||||
| 	BaseDomain         string `mapstructure:"base_domain"` | ||||
| 	Nameservers        Nameservers | ||||
| 	SearchDomains      []string            `mapstructure:"search_domains"` | ||||
| 	ExtraRecords       []tailcfg.DNSRecord `mapstructure:"extra_records"` | ||||
| 	UserNameInMagicDNS bool                `mapstructure:"use_username_in_magic_dns"` | ||||
| } | ||||
| 
 | ||||
| type Nameservers struct { | ||||
| 	Global []string | ||||
| 	Split  map[string][]string | ||||
| } | ||||
| 
 | ||||
| type SqliteConfig struct { | ||||
| 	Path          string | ||||
| 	WriteAheadLog bool | ||||
| @ -201,7 +216,8 @@ func LoadConfig(path string, isFile bool) error { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	viper.SetEnvPrefix("headscale") | ||||
| 	envPrefix := "headscale" | ||||
| 	viper.SetEnvPrefix(envPrefix) | ||||
| 	viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) | ||||
| 	viper.AutomaticEnv() | ||||
| 
 | ||||
| @ -213,9 +229,13 @@ func LoadConfig(path string, isFile bool) error { | ||||
| 	viper.SetDefault("log.level", "info") | ||||
| 	viper.SetDefault("log.format", TextLogFormat) | ||||
| 
 | ||||
| 	viper.SetDefault("dns_config", nil) | ||||
| 	viper.SetDefault("dns_config.override_local_dns", true) | ||||
| 	viper.SetDefault("dns_config.use_username_in_magic_dns", false) | ||||
| 	viper.SetDefault("dns.magic_dns", true) | ||||
| 	viper.SetDefault("dns.base_domain", "") | ||||
| 	viper.SetDefault("dns.nameservers.global", []string{}) | ||||
| 	viper.SetDefault("dns.nameservers.split", map[string]string{}) | ||||
| 	viper.SetDefault("dns.search_domains", []string{}) | ||||
| 	viper.SetDefault("dns.extra_records", []tailcfg.DNSRecord{}) | ||||
| 	viper.SetDefault("dns.use_username_in_magic_dns", false) | ||||
| 
 | ||||
| 	viper.SetDefault("derp.server.enabled", false) | ||||
| 	viper.SetDefault("derp.server.stun.enabled", true) | ||||
| @ -259,17 +279,33 @@ func LoadConfig(path string, isFile bool) error { | ||||
| 	} | ||||
| 
 | ||||
| 	if err := viper.ReadInConfig(); err != nil { | ||||
| 		log.Warn().Err(err).Msg("Failed to read configuration from disk") | ||||
| 
 | ||||
| 		return fmt.Errorf("fatal error reading config file: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	depr := deprecator{ | ||||
| 		warns:  make(set.Set[string]), | ||||
| 		fatals: make(set.Set[string]), | ||||
| 	} | ||||
| 
 | ||||
| 	// Register aliases for backward compatibility
 | ||||
| 	// Has to be called _after_ viper.ReadInConfig()
 | ||||
| 	// https://github.com/spf13/viper/issues/560
 | ||||
| 
 | ||||
| 	// Alias the old ACL Policy path with the new configuration option.
 | ||||
| 	registerAliasAndDeprecate("policy.path", "acl_policy_path") | ||||
| 	depr.warnWithAlias("policy.path", "acl_policy_path") | ||||
| 
 | ||||
| 	// Move dns_config -> dns
 | ||||
| 	depr.warn("dns_config.override_local_dns") | ||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.magic_dns", "dns_config.magic_dns") | ||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.base_domain", "dns_config.base_domain") | ||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.nameservers.global", "dns_config.nameservers") | ||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.nameservers.split", "dns_config.restricted_nameservers") | ||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.search_domains", "dns_config.domains") | ||||
| 	depr.fatalIfNewKeyIsNotUsed("dns.extra_records", "dns_config.extra_records") | ||||
| 	depr.warn("dns_config.use_username_in_magic_dns") | ||||
| 	depr.warn("dns.use_username_in_magic_dns") | ||||
| 
 | ||||
| 	depr.Log() | ||||
| 
 | ||||
| 	// Collect any validation errors and return them all at once
 | ||||
| 	var errorText string | ||||
| @ -485,123 +521,131 @@ func GetDatabaseConfig() DatabaseConfig { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func GetDNSConfig() (*tailcfg.DNSConfig, string) { | ||||
| 	if viper.IsSet("dns_config") { | ||||
| 		dnsConfig := &tailcfg.DNSConfig{} | ||||
| func DNS() (DNSConfig, error) { | ||||
| 	var dns DNSConfig | ||||
| 
 | ||||
| 		overrideLocalDNS := viper.GetBool("dns_config.override_local_dns") | ||||
| 	// TODO: Use this instead of manually getting settings when
 | ||||
| 	// UnmarshalKey is compatible with Environment Variables.
 | ||||
| 	// err := viper.UnmarshalKey("dns", &dns)
 | ||||
| 	// if err != nil {
 | ||||
| 	// 	return DNSConfig{}, fmt.Errorf("unmarshaling dns config: %w", err)
 | ||||
| 	// }
 | ||||
| 
 | ||||
| 		if viper.IsSet("dns_config.nameservers") { | ||||
| 			nameserversStr := viper.GetStringSlice("dns_config.nameservers") | ||||
| 	dns.MagicDNS = viper.GetBool("dns.magic_dns") | ||||
| 	dns.BaseDomain = viper.GetString("dns.base_domain") | ||||
| 	dns.Nameservers.Global = viper.GetStringSlice("dns.nameservers.global") | ||||
| 	dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split") | ||||
| 	dns.SearchDomains = viper.GetStringSlice("dns.search_domains") | ||||
| 
 | ||||
| 			nameservers := []netip.Addr{} | ||||
| 			resolvers := []*dnstype.Resolver{} | ||||
| 	if viper.IsSet("dns.extra_records") { | ||||
| 		var extraRecords []tailcfg.DNSRecord | ||||
| 
 | ||||
| 			for _, nameserverStr := range nameserversStr { | ||||
| 				// Search for explicit DNS-over-HTTPS resolvers
 | ||||
| 				if strings.HasPrefix(nameserverStr, "https://") { | ||||
| 					resolvers = append(resolvers, &dnstype.Resolver{ | ||||
| 						Addr: nameserverStr, | ||||
| 					}) | ||||
| 
 | ||||
| 					// This nameserver can not be parsed as an IP address
 | ||||
| 					continue | ||||
| 				} | ||||
| 
 | ||||
| 				// Parse nameserver as a regular IP
 | ||||
| 				nameserver, err := netip.ParseAddr(nameserverStr) | ||||
| 				if err != nil { | ||||
| 					log.Error(). | ||||
| 						Str("func", "getDNSConfig"). | ||||
| 						Err(err). | ||||
| 						Msgf("Could not parse nameserver IP: %s", nameserverStr) | ||||
| 				} | ||||
| 
 | ||||
| 				nameservers = append(nameservers, nameserver) | ||||
| 				resolvers = append(resolvers, &dnstype.Resolver{ | ||||
| 					Addr: nameserver.String(), | ||||
| 				}) | ||||
| 			} | ||||
| 
 | ||||
| 			dnsConfig.Nameservers = nameservers | ||||
| 
 | ||||
| 			if overrideLocalDNS { | ||||
| 				dnsConfig.Resolvers = resolvers | ||||
| 			} else { | ||||
| 				dnsConfig.FallbackResolvers = resolvers | ||||
| 			} | ||||
| 		err := viper.UnmarshalKey("dns.extra_records", &extraRecords) | ||||
| 		if err != nil { | ||||
| 			return DNSConfig{}, fmt.Errorf("unmarshaling dns extra records: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		if viper.IsSet("dns_config.restricted_nameservers") { | ||||
| 			dnsConfig.Routes = make(map[string][]*dnstype.Resolver) | ||||
| 			domains := []string{} | ||||
| 			restrictedDNS := viper.GetStringMapStringSlice( | ||||
| 				"dns_config.restricted_nameservers", | ||||
| 			) | ||||
| 			for domain, restrictedNameservers := range restrictedDNS { | ||||
| 				restrictedResolvers := make( | ||||
| 					[]*dnstype.Resolver, | ||||
| 					len(restrictedNameservers), | ||||
| 				) | ||||
| 				for index, nameserverStr := range restrictedNameservers { | ||||
| 					nameserver, err := netip.ParseAddr(nameserverStr) | ||||
| 					if err != nil { | ||||
| 						log.Error(). | ||||
| 							Str("func", "getDNSConfig"). | ||||
| 							Err(err). | ||||
| 							Msgf("Could not parse restricted nameserver IP: %s", nameserverStr) | ||||
| 					} | ||||
| 					restrictedResolvers[index] = &dnstype.Resolver{ | ||||
| 						Addr: nameserver.String(), | ||||
| 					} | ||||
| 				} | ||||
| 				dnsConfig.Routes[domain] = restrictedResolvers | ||||
| 				domains = append(domains, domain) | ||||
| 			} | ||||
| 			dnsConfig.Domains = domains | ||||
| 		} | ||||
| 
 | ||||
| 		if viper.IsSet("dns_config.extra_records") { | ||||
| 			var extraRecords []tailcfg.DNSRecord | ||||
| 
 | ||||
| 			err := viper.UnmarshalKey("dns_config.extra_records", &extraRecords) | ||||
| 			if err != nil { | ||||
| 				log.Error(). | ||||
| 					Str("func", "getDNSConfig"). | ||||
| 					Err(err). | ||||
| 					Msgf("Could not parse dns_config.extra_records") | ||||
| 			} | ||||
| 
 | ||||
| 			dnsConfig.ExtraRecords = extraRecords | ||||
| 		} | ||||
| 
 | ||||
| 		if viper.IsSet("dns_config.magic_dns") { | ||||
| 			dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") | ||||
| 		} | ||||
| 
 | ||||
| 		var baseDomain string | ||||
| 		if viper.IsSet("dns_config.base_domain") { | ||||
| 			baseDomain = viper.GetString("dns_config.base_domain") | ||||
| 		} else { | ||||
| 			baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
 | ||||
| 		} | ||||
| 
 | ||||
| 		if !viper.GetBool("dns_config.use_username_in_magic_dns") { | ||||
| 			dnsConfig.Domains = []string{baseDomain} | ||||
| 		} else { | ||||
| 			log.Warn().Msg("DNS: Usernames in DNS has been deprecated, this option will be remove in future versions") | ||||
| 			log.Warn().Msg("DNS: see 0.23.0 changelog for more information.") | ||||
| 		} | ||||
| 
 | ||||
| 		if domains := viper.GetStringSlice("dns_config.domains"); len(domains) > 0 { | ||||
| 			dnsConfig.Domains = append(dnsConfig.Domains, domains...) | ||||
| 		} | ||||
| 
 | ||||
| 		log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded") | ||||
| 		return dnsConfig, baseDomain | ||||
| 		dns.ExtraRecords = extraRecords | ||||
| 	} | ||||
| 
 | ||||
| 	return nil, "" | ||||
| 	dns.UserNameInMagicDNS = viper.GetBool("dns.use_username_in_magic_dns") | ||||
| 
 | ||||
| 	return dns, nil | ||||
| } | ||||
| 
 | ||||
| // GlobalResolvers returns the global DNS resolvers
 | ||||
| // defined in the config file.
 | ||||
| // If a nameserver is a valid IP, it will be used as a regular resolver.
 | ||||
| // If a nameserver is a valid URL, it will be used as a DoH resolver.
 | ||||
| // If a nameserver is neither a valid URL nor a valid IP, it will be ignored.
 | ||||
| func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { | ||||
| 	var resolvers []*dnstype.Resolver | ||||
| 
 | ||||
| 	for _, nsStr := range d.Nameservers.Global { | ||||
| 		warn := "" | ||||
| 		if _, err := netip.ParseAddr(nsStr); err == nil { | ||||
| 			resolvers = append(resolvers, &dnstype.Resolver{ | ||||
| 				Addr: nsStr, | ||||
| 			}) | ||||
| 
 | ||||
| 			continue | ||||
| 		} else { | ||||
| 			warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err := url.Parse(nsStr); err == nil { | ||||
| 			resolvers = append(resolvers, &dnstype.Resolver{ | ||||
| 				Addr: nsStr, | ||||
| 			}) | ||||
| 		} else { | ||||
| 			warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) | ||||
| 		} | ||||
| 
 | ||||
| 		if warn != "" { | ||||
| 			log.Warn().Msg(warn) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return resolvers | ||||
| } | ||||
| 
 | ||||
| // SplitResolvers returns a map of domain to DNS resolvers.
 | ||||
| // If a nameserver is a valid IP, it will be used as a regular resolver.
 | ||||
| // If a nameserver is a valid URL, it will be used as a DoH resolver.
 | ||||
| // If a nameserver is neither a valid URL nor a valid IP, it will be ignored.
 | ||||
| func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { | ||||
| 	routes := make(map[string][]*dnstype.Resolver) | ||||
| 	for domain, nameservers := range d.Nameservers.Split { | ||||
| 		var resolvers []*dnstype.Resolver | ||||
| 		for _, nsStr := range nameservers { | ||||
| 			warn := "" | ||||
| 			if _, err := netip.ParseAddr(nsStr); err == nil { | ||||
| 				resolvers = append(resolvers, &dnstype.Resolver{ | ||||
| 					Addr: nsStr, | ||||
| 				}) | ||||
| 
 | ||||
| 				continue | ||||
| 			} else { | ||||
| 				warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) | ||||
| 			} | ||||
| 
 | ||||
| 			if _, err := url.Parse(nsStr); err == nil { | ||||
| 				resolvers = append(resolvers, &dnstype.Resolver{ | ||||
| 					Addr: nsStr, | ||||
| 				}) | ||||
| 			} else { | ||||
| 				warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) | ||||
| 			} | ||||
| 
 | ||||
| 			if warn != "" { | ||||
| 				log.Warn().Msg(warn) | ||||
| 			} | ||||
| 		} | ||||
| 		routes[domain] = resolvers | ||||
| 	} | ||||
| 
 | ||||
| 	return routes | ||||
| } | ||||
| 
 | ||||
| func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { | ||||
| 	cfg := tailcfg.DNSConfig{} | ||||
| 
 | ||||
| 	if dns.BaseDomain == "" && dns.MagicDNS { | ||||
| 		log.Fatal().Msg("dns.base_domain must be set when using MagicDNS (dns.magic_dns)") | ||||
| 	} | ||||
| 
 | ||||
| 	cfg.Proxied = dns.MagicDNS | ||||
| 	cfg.ExtraRecords = dns.ExtraRecords | ||||
| 	cfg.Resolvers = dns.GlobalResolvers() | ||||
| 
 | ||||
| 	routes := dns.SplitResolvers() | ||||
| 	cfg.Routes = routes | ||||
| 	if dns.BaseDomain != "" { | ||||
| 		cfg.Domains = []string{dns.BaseDomain} | ||||
| 	} | ||||
| 	cfg.Domains = append(cfg.Domains, dns.SearchDomains...) | ||||
| 
 | ||||
| 	return &cfg | ||||
| } | ||||
| 
 | ||||
| func PrefixV4() (*netip.Prefix, error) { | ||||
| @ -693,7 +737,11 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 		return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) | ||||
| 	} | ||||
| 
 | ||||
| 	dnsConfig, baseDomain := GetDNSConfig() | ||||
| 	dnsConfig, err := DNS() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	derpConfig := GetDERPConfig() | ||||
| 	logTailConfig := GetLogTailConfig() | ||||
| 	randomizeClientPort := viper.GetBool("randomize_client_port") | ||||
| @ -711,8 +759,23 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 		oidcClientSecret = strings.TrimSpace(string(secretBytes)) | ||||
| 	} | ||||
| 
 | ||||
| 	serverURL := viper.GetString("server_url") | ||||
| 
 | ||||
| 	// BaseDomain cannot be the same as the server URL.
 | ||||
| 	// This is because Tailscale takes over the domain in BaseDomain,
 | ||||
| 	// causing the headscale server and DERP to be unreachable.
 | ||||
| 	// For Tailscale upstream, the following is true:
 | ||||
| 	// - DERP run on their own domains
 | ||||
| 	// - Control plane runs on login.tailscale.com/controlplane.tailscale.com
 | ||||
| 	// - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net)
 | ||||
| 	//
 | ||||
| 	// TODO(kradalby): remove dnsConfig.UserNameInMagicDNS check when removed.
 | ||||
| 	if !dnsConfig.UserNameInMagicDNS && dnsConfig.BaseDomain != "" && strings.Contains(serverURL, dnsConfig.BaseDomain) { | ||||
| 		return nil, errors.New("server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.") | ||||
| 	} | ||||
| 
 | ||||
| 	return &Config{ | ||||
| 		ServerURL:          viper.GetString("server_url"), | ||||
| 		ServerURL:          serverURL, | ||||
| 		Addr:               viper.GetString("listen_addr"), | ||||
| 		MetricsAddr:        viper.GetString("metrics_listen_addr"), | ||||
| 		GRPCAddr:           viper.GetString("grpc_listen_addr"), | ||||
| @ -726,7 +789,7 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 		NoisePrivateKeyPath: util.AbsolutePathFromConfigPath( | ||||
| 			viper.GetString("noise.private_key_path"), | ||||
| 		), | ||||
| 		BaseDomain: baseDomain, | ||||
| 		BaseDomain: dnsConfig.BaseDomain, | ||||
| 
 | ||||
| 		DERP: derpConfig, | ||||
| 
 | ||||
| @ -738,8 +801,8 @@ func GetHeadscaleConfig() (*Config, error) { | ||||
| 
 | ||||
| 		TLS: GetTLSConfig(), | ||||
| 
 | ||||
| 		DNSConfig:             dnsConfig, | ||||
| 		DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"), | ||||
| 		DNSConfig:             DNSToTailcfgDNS(dnsConfig), | ||||
| 		DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS, | ||||
| 
 | ||||
| 		ACMEEmail: viper.GetString("acme_email"), | ||||
| 		ACMEURL:   viper.GetString("acme_url"), | ||||
| @ -805,19 +868,70 @@ func IsCLIConfigured() bool { | ||||
| 	return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" | ||||
| } | ||||
| 
 | ||||
| // registerAliasAndDeprecate will register an alias between the newKey and the oldKey,
 | ||||
| type deprecator struct { | ||||
| 	warns  set.Set[string] | ||||
| 	fatals set.Set[string] | ||||
| } | ||||
| 
 | ||||
| // warnWithAlias will register an alias between the newKey and the oldKey,
 | ||||
| // and log a deprecation warning if the oldKey is set.
 | ||||
| func registerAliasAndDeprecate(newKey, oldKey string) { | ||||
| func (d *deprecator) warnWithAlias(newKey, oldKey string) { | ||||
| 	// NOTE: RegisterAlias is called with NEW KEY -> OLD KEY
 | ||||
| 	viper.RegisterAlias(newKey, oldKey) | ||||
| 	if viper.IsSet(oldKey) { | ||||
| 		log.Warn().Msgf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey) | ||||
| 		d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // deprecateAndFatal will log a fatal deprecation warning if the oldKey is set.
 | ||||
| func deprecateAndFatal(newKey, oldKey string) { | ||||
| // fatal deprecates and adds an entry to the fatal list of options if the oldKey is set.
 | ||||
| func (d *deprecator) fatal(newKey, oldKey string) { | ||||
| 	if viper.IsSet(oldKey) { | ||||
| 		log.Fatal().Msgf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey) | ||||
| 		d.fatals.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // fatalIfNewKeyIsNotUsed deprecates and adds an entry to the fatal list of options if the oldKey is set and the new key is _not_ set.
 | ||||
| // If the new key is set, a warning is emitted instead.
 | ||||
| func (d *deprecator) fatalIfNewKeyIsNotUsed(newKey, oldKey string) { | ||||
| 	if viper.IsSet(oldKey) && !viper.IsSet(newKey) { | ||||
| 		d.fatals.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) | ||||
| 	} else if viper.IsSet(oldKey) { | ||||
| 		d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // warn deprecates and adds an option to log a warning if the oldKey is set.
 | ||||
| func (d *deprecator) warnNoAlias(newKey, oldKey string) { | ||||
| 	if viper.IsSet(oldKey) { | ||||
| 		d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // warn deprecates and adds an entry to the warn list of options if the oldKey is set.
 | ||||
| func (d *deprecator) warn(oldKey string) { | ||||
| 	if viper.IsSet(oldKey) { | ||||
| 		d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated and has been removed. Please see the changelog for more details.", oldKey)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (d *deprecator) String() string { | ||||
| 	var b strings.Builder | ||||
| 
 | ||||
| 	for _, w := range d.warns.Slice() { | ||||
| 		fmt.Fprintf(&b, "WARN: %s\n", w) | ||||
| 	} | ||||
| 
 | ||||
| 	for _, f := range d.fatals.Slice() { | ||||
| 		fmt.Fprintf(&b, "FATAL: %s\n", f) | ||||
| 	} | ||||
| 
 | ||||
| 	return b.String() | ||||
| } | ||||
| 
 | ||||
| func (d *deprecator) Log() { | ||||
| 	if len(d.fatals) > 0 { | ||||
| 		log.Fatal().Msg("\n" + d.String()) | ||||
| 	} else if len(d.warns) > 0 { | ||||
| 		log.Warn().Msg("\n" + d.String()) | ||||
| 	} | ||||
| } | ||||
|  | ||||
							
								
								
									
										272
									
								
								hscontrol/types/config_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										272
									
								
								hscontrol/types/config_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,272 @@ | ||||
| package types | ||||
| 
 | ||||
| import ( | ||||
| 	"testing" | ||||
| 
 | ||||
| 	"github.com/google/go-cmp/cmp" | ||||
| 	"github.com/spf13/viper" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"tailscale.com/tailcfg" | ||||
| 	"tailscale.com/types/dnstype" | ||||
| ) | ||||
| 
 | ||||
| func TestReadConfig(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name       string | ||||
| 		configPath string | ||||
| 		setup      func(*testing.T) (any, error) | ||||
| 		want       any | ||||
| 		wantErr    string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:       "unmarshal-dns-full-config", | ||||
| 			configPath: "testdata/dns_full.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return dns, nil | ||||
| 			}, | ||||
| 			want: DNSConfig{ | ||||
| 				MagicDNS:   true, | ||||
| 				BaseDomain: "example.com", | ||||
| 				Nameservers: Nameservers{ | ||||
| 					Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"}, | ||||
| 					Split:  map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}}, | ||||
| 				}, | ||||
| 				ExtraRecords: []tailcfg.DNSRecord{ | ||||
| 					{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, | ||||
| 					{Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, | ||||
| 				}, | ||||
| 				SearchDomains:      []string{"test.com", "bar.com"}, | ||||
| 				UserNameInMagicDNS: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "dns-to-tailcfg.DNSConfig", | ||||
| 			configPath: "testdata/dns_full.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return DNSToTailcfgDNS(dns), nil | ||||
| 			}, | ||||
| 			want: &tailcfg.DNSConfig{ | ||||
| 				Proxied: true, | ||||
| 				Domains: []string{"example.com", "test.com", "bar.com"}, | ||||
| 				Resolvers: []*dnstype.Resolver{ | ||||
| 					{Addr: "1.1.1.1"}, | ||||
| 					{Addr: "1.0.0.1"}, | ||||
| 					{Addr: "2606:4700:4700::1111"}, | ||||
| 					{Addr: "2606:4700:4700::1001"}, | ||||
| 					{Addr: "https://dns.nextdns.io/abc123"}, | ||||
| 				}, | ||||
| 				Routes: map[string][]*dnstype.Resolver{ | ||||
| 					"darp.headscale.net": {{Addr: "1.1.1.1"}, {Addr: "8.8.8.8"}}, | ||||
| 					"foo.bar.com":        {{Addr: "1.1.1.1"}}, | ||||
| 				}, | ||||
| 				ExtraRecords: []tailcfg.DNSRecord{ | ||||
| 					{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, | ||||
| 					{Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "unmarshal-dns-full-no-magic", | ||||
| 			configPath: "testdata/dns_full_no_magic.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return dns, nil | ||||
| 			}, | ||||
| 			want: DNSConfig{ | ||||
| 				MagicDNS:   false, | ||||
| 				BaseDomain: "example.com", | ||||
| 				Nameservers: Nameservers{ | ||||
| 					Global: []string{"1.1.1.1", "1.0.0.1", "2606:4700:4700::1111", "2606:4700:4700::1001", "https://dns.nextdns.io/abc123"}, | ||||
| 					Split:  map[string][]string{"darp.headscale.net": {"1.1.1.1", "8.8.8.8"}, "foo.bar.com": {"1.1.1.1"}}, | ||||
| 				}, | ||||
| 				ExtraRecords: []tailcfg.DNSRecord{ | ||||
| 					{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, | ||||
| 					{Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, | ||||
| 				}, | ||||
| 				SearchDomains:      []string{"test.com", "bar.com"}, | ||||
| 				UserNameInMagicDNS: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "dns-to-tailcfg.DNSConfig", | ||||
| 			configPath: "testdata/dns_full_no_magic.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				dns, err := DNS() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return DNSToTailcfgDNS(dns), nil | ||||
| 			}, | ||||
| 			want: &tailcfg.DNSConfig{ | ||||
| 				Proxied: false, | ||||
| 				Domains: []string{"example.com", "test.com", "bar.com"}, | ||||
| 				Resolvers: []*dnstype.Resolver{ | ||||
| 					{Addr: "1.1.1.1"}, | ||||
| 					{Addr: "1.0.0.1"}, | ||||
| 					{Addr: "2606:4700:4700::1111"}, | ||||
| 					{Addr: "2606:4700:4700::1001"}, | ||||
| 					{Addr: "https://dns.nextdns.io/abc123"}, | ||||
| 				}, | ||||
| 				Routes: map[string][]*dnstype.Resolver{ | ||||
| 					"darp.headscale.net": {{Addr: "1.1.1.1"}, {Addr: "8.8.8.8"}}, | ||||
| 					"foo.bar.com":        {{Addr: "1.1.1.1"}}, | ||||
| 				}, | ||||
| 				ExtraRecords: []tailcfg.DNSRecord{ | ||||
| 					{Name: "grafana.myvpn.example.com", Type: "A", Value: "100.64.0.3"}, | ||||
| 					{Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"}, | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "base-domain-in-server-url-err", | ||||
| 			configPath: "testdata/base-domain-in-server-url.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				return GetHeadscaleConfig() | ||||
| 			}, | ||||
| 			want:    nil, | ||||
| 			wantErr: "server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:       "base-domain-not-in-server-url", | ||||
| 			configPath: "testdata/base-domain-not-in-server-url.yaml", | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				cfg, err := GetHeadscaleConfig() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return map[string]string{ | ||||
| 					"server_url":  cfg.ServerURL, | ||||
| 					"base_domain": cfg.BaseDomain, | ||||
| 				}, err | ||||
| 			}, | ||||
| 			want: map[string]string{ | ||||
| 				"server_url":  "https://derp.no", | ||||
| 				"base_domain": "clients.derp.no", | ||||
| 			}, | ||||
| 			wantErr: "", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			viper.Reset() | ||||
| 			err := LoadConfig(tt.configPath, true) | ||||
| 			assert.NoError(t, err) | ||||
| 
 | ||||
| 			conf, err := tt.setup(t) | ||||
| 
 | ||||
| 			if tt.wantErr != "" { | ||||
| 				assert.Equal(t, tt.wantErr, err.Error()) | ||||
| 
 | ||||
| 				return | ||||
| 			} | ||||
| 
 | ||||
| 			assert.NoError(t, err) | ||||
| 
 | ||||
| 			if diff := cmp.Diff(tt.want, conf); diff != "" { | ||||
| 				t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestReadConfigFromEnv(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name      string | ||||
| 		configEnv map[string]string | ||||
| 		setup     func(*testing.T) (any, error) | ||||
| 		want      any | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "test-random-base-settings-with-env", | ||||
| 			configEnv: map[string]string{ | ||||
| 				"HEADSCALE_LOG_LEVEL":                       "trace", | ||||
| 				"HEADSCALE_DATABASE_SQLITE_WRITE_AHEAD_LOG": "false", | ||||
| 				"HEADSCALE_PREFIXES_V4":                     "100.64.0.0/10", | ||||
| 			}, | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				t.Logf("all settings: %#v", viper.AllSettings()) | ||||
| 
 | ||||
| 				assert.Equal(t, "trace", viper.GetString("log.level")) | ||||
| 				assert.Equal(t, "100.64.0.0/10", viper.GetString("prefixes.v4")) | ||||
| 				assert.False(t, viper.GetBool("database.sqlite.write_ahead_log")) | ||||
| 				return nil, nil | ||||
| 			}, | ||||
| 			want: nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "unmarshal-dns-full-config", | ||||
| 			configEnv: map[string]string{ | ||||
| 				"HEADSCALE_DNS_MAGIC_DNS":                 "true", | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":               "example.com", | ||||
| 				"HEADSCALE_DNS_NAMESERVERS_GLOBAL":        `1.1.1.1 8.8.8.8`, | ||||
| 				"HEADSCALE_DNS_SEARCH_DOMAINS":            "test.com bar.com", | ||||
| 				"HEADSCALE_DNS_USE_USERNAME_IN_MAGIC_DNS": "true", | ||||
| 
 | ||||
| 				// TODO(kradalby): Figure out how to pass these as env vars
 | ||||
| 				// "HEADSCALE_DNS_NAMESERVERS_SPLIT":  `{foo.bar.com: ["1.1.1.1"]}`,
 | ||||
| 				// "HEADSCALE_DNS_EXTRA_RECORDS":      `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`,
 | ||||
| 			}, | ||||
| 			setup: func(t *testing.T) (any, error) { | ||||
| 				t.Logf("all settings: %#v", viper.AllSettings()) | ||||
| 
 | ||||
| 				dns, err := DNS() | ||||
| 				if err != nil { | ||||
| 					return nil, err | ||||
| 				} | ||||
| 
 | ||||
| 				return dns, nil | ||||
| 			}, | ||||
| 			want: DNSConfig{ | ||||
| 				MagicDNS:   true, | ||||
| 				BaseDomain: "example.com", | ||||
| 				Nameservers: Nameservers{ | ||||
| 					Global: []string{"1.1.1.1", "8.8.8.8"}, | ||||
| 					Split:  map[string][]string{ | ||||
| 						// "foo.bar.com": {"1.1.1.1"},
 | ||||
| 					}, | ||||
| 				}, | ||||
| 				ExtraRecords: []tailcfg.DNSRecord{ | ||||
| 					// {Name: "prometheus.myvpn.example.com", Type: "A", Value: "100.64.0.4"},
 | ||||
| 				}, | ||||
| 				SearchDomains:      []string{"test.com", "bar.com"}, | ||||
| 				UserNameInMagicDNS: true, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			for k, v := range tt.configEnv { | ||||
| 				t.Setenv(k, v) | ||||
| 			} | ||||
| 
 | ||||
| 			viper.Reset() | ||||
| 			err := LoadConfig("testdata/minimal.yaml", true) | ||||
| 			assert.NoError(t, err) | ||||
| 
 | ||||
| 			conf, err := tt.setup(t) | ||||
| 			assert.NoError(t, err) | ||||
| 
 | ||||
| 			if diff := cmp.Diff(tt.want, conf); diff != "" { | ||||
| 				t.Errorf("ReadConfig() mismatch (-want +got):\n%s", diff) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @ -394,40 +394,39 @@ func (node *Node) Proto() *v1.Node { | ||||
| } | ||||
| 
 | ||||
| func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) { | ||||
| 	var hostname string | ||||
| 	if cfg.DNSConfig != nil && cfg.DNSConfig.Proxied { // MagicDNS
 | ||||
| 		if node.GivenName == "" { | ||||
| 			return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) | ||||
| 		} | ||||
| 	if node.GivenName == "" { | ||||
| 		return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) | ||||
| 	} | ||||
| 
 | ||||
| 	hostname := node.GivenName | ||||
| 
 | ||||
| 	if baseDomain != "" { | ||||
| 		hostname = fmt.Sprintf( | ||||
| 			"%s.%s", | ||||
| 			node.GivenName, | ||||
| 			baseDomain, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 		if cfg.DNSUserNameInMagicDNS { | ||||
| 			if node.User.Name == "" { | ||||
| 				return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) | ||||
| 			} | ||||
| 
 | ||||
| 			hostname = fmt.Sprintf( | ||||
| 				"%s.%s.%s", | ||||
| 				node.GivenName, | ||||
| 				node.User.Name, | ||||
| 				baseDomain, | ||||
| 			) | ||||
| 	if cfg.DNSUserNameInMagicDNS { | ||||
| 		if node.User.Name == "" { | ||||
| 			return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) | ||||
| 		} | ||||
| 
 | ||||
| 		if len(hostname) > MaxHostnameLength { | ||||
| 			return "", fmt.Errorf( | ||||
| 				"failed to create valid FQDN (%s): %w", | ||||
| 				hostname, | ||||
| 				ErrHostnameTooLong, | ||||
| 			) | ||||
| 		} | ||||
| 	} else { | ||||
| 		hostname = node.GivenName | ||||
| 		hostname = fmt.Sprintf( | ||||
| 			"%s.%s.%s", | ||||
| 			node.GivenName, | ||||
| 			node.User.Name, | ||||
| 			baseDomain, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	if len(hostname) > MaxHostnameLength { | ||||
| 		return "", fmt.Errorf( | ||||
| 			"failed to create valid FQDN (%s): %w", | ||||
| 			hostname, | ||||
| 			ErrHostnameTooLong, | ||||
| 		) | ||||
| 	} | ||||
| 
 | ||||
| 	return hostname, nil | ||||
|  | ||||
| @ -195,7 +195,7 @@ func TestNodeFQDN(t *testing.T) { | ||||
| 				DNSUserNameInMagicDNS: true, | ||||
| 			}, | ||||
| 			domain: "example.com", | ||||
| 			want:   "test", | ||||
| 			want:   "test.user.example.com", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "no-dnsconfig-with-username", | ||||
| @ -206,7 +206,7 @@ func TestNodeFQDN(t *testing.T) { | ||||
| 				}, | ||||
| 			}, | ||||
| 			domain: "example.com", | ||||
| 			want:   "test", | ||||
| 			want:   "test.example.com", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "all-set", | ||||
| @ -271,7 +271,7 @@ func TestNodeFQDN(t *testing.T) { | ||||
| 				DNSUserNameInMagicDNS: false, | ||||
| 			}, | ||||
| 			domain: "example.com", | ||||
| 			want:   "test", | ||||
| 			want:   "test.example.com", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "no-dnsconfig", | ||||
| @ -282,7 +282,7 @@ func TestNodeFQDN(t *testing.T) { | ||||
| 				}, | ||||
| 			}, | ||||
| 			domain: "example.com", | ||||
| 			want:   "test", | ||||
| 			want:   "test.example.com", | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										16
									
								
								hscontrol/types/testdata/base-domain-in-server-url.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								hscontrol/types/testdata/base-domain-in-server-url.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| noise: | ||||
|   private_key_path: "private_key.pem" | ||||
| 
 | ||||
| prefixes: | ||||
|   v6: fd7a:115c:a1e0::/48 | ||||
|   v4: 100.64.0.0/10 | ||||
| 
 | ||||
| database: | ||||
|   type: sqlite3 | ||||
| 
 | ||||
| server_url: "https://derp.no" | ||||
| 
 | ||||
| dns: | ||||
|   magic_dns: true | ||||
|   base_domain: derp.no | ||||
|   use_username_in_magic_dns: false | ||||
							
								
								
									
										16
									
								
								hscontrol/types/testdata/base-domain-not-in-server-url.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								hscontrol/types/testdata/base-domain-not-in-server-url.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| noise: | ||||
|   private_key_path: "private_key.pem" | ||||
| 
 | ||||
| prefixes: | ||||
|   v6: fd7a:115c:a1e0::/48 | ||||
|   v4: 100.64.0.0/10 | ||||
| 
 | ||||
| database: | ||||
|   type: sqlite3 | ||||
| 
 | ||||
| server_url: "https://derp.no" | ||||
| 
 | ||||
| dns: | ||||
|   magic_dns: true | ||||
|   base_domain: clients.derp.no | ||||
|   use_username_in_magic_dns: false | ||||
							
								
								
									
										37
									
								
								hscontrol/types/testdata/dns_full.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								hscontrol/types/testdata/dns_full.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| # minimum to not fatal | ||||
| noise: | ||||
|   private_key_path: "private_key.pem" | ||||
| server_url: "https://derp.no" | ||||
| 
 | ||||
| dns: | ||||
|   magic_dns: true | ||||
|   base_domain: example.com | ||||
| 
 | ||||
|   nameservers: | ||||
|     global: | ||||
|       - 1.1.1.1 | ||||
|       - 1.0.0.1 | ||||
|       - 2606:4700:4700::1111 | ||||
|       - 2606:4700:4700::1001 | ||||
|       - https://dns.nextdns.io/abc123 | ||||
| 
 | ||||
|     split: | ||||
|       foo.bar.com: | ||||
|         - 1.1.1.1 | ||||
|       darp.headscale.net: | ||||
|         - 1.1.1.1 | ||||
|         - 8.8.8.8 | ||||
| 
 | ||||
|   search_domains: | ||||
|     - test.com | ||||
|     - bar.com | ||||
| 
 | ||||
|   extra_records: | ||||
|     - name: "grafana.myvpn.example.com" | ||||
|       type: "A" | ||||
|       value: "100.64.0.3" | ||||
| 
 | ||||
|     # you can also put it in one line | ||||
|     - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" } | ||||
| 
 | ||||
|   use_username_in_magic_dns: true | ||||
							
								
								
									
										37
									
								
								hscontrol/types/testdata/dns_full_no_magic.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								hscontrol/types/testdata/dns_full_no_magic.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | ||||
| # minimum to not fatal | ||||
| noise: | ||||
|   private_key_path: "private_key.pem" | ||||
| server_url: "https://derp.no" | ||||
| 
 | ||||
| dns: | ||||
|   magic_dns: false | ||||
|   base_domain: example.com | ||||
| 
 | ||||
|   nameservers: | ||||
|     global: | ||||
|       - 1.1.1.1 | ||||
|       - 1.0.0.1 | ||||
|       - 2606:4700:4700::1111 | ||||
|       - 2606:4700:4700::1001 | ||||
|       - https://dns.nextdns.io/abc123 | ||||
| 
 | ||||
|     split: | ||||
|       foo.bar.com: | ||||
|         - 1.1.1.1 | ||||
|       darp.headscale.net: | ||||
|         - 1.1.1.1 | ||||
|         - 8.8.8.8 | ||||
| 
 | ||||
|   search_domains: | ||||
|     - test.com | ||||
|     - bar.com | ||||
| 
 | ||||
|   extra_records: | ||||
|     - name: "grafana.myvpn.example.com" | ||||
|       type: "A" | ||||
|       value: "100.64.0.3" | ||||
| 
 | ||||
|     # you can also put it in one line | ||||
|     - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" } | ||||
| 
 | ||||
|   use_username_in_magic_dns: true | ||||
							
								
								
									
										3
									
								
								hscontrol/types/testdata/minimal.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								hscontrol/types/testdata/minimal.yaml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | ||||
| noise: | ||||
|   private_key_path: "private_key.pem" | ||||
| server_url: "https://derp.no" | ||||
							
								
								
									
										246
									
								
								integration/dns_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								integration/dns_test.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,246 @@ | ||||
| package integration | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
| 	"time" | ||||
| 
 | ||||
| 	"github.com/juanfont/headscale/integration/hsic" | ||||
| 	"github.com/juanfont/headscale/integration/tsic" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
| 
 | ||||
| func TestResolveMagicDNS(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 	t.Parallel() | ||||
| 
 | ||||
| 	scenario, err := NewScenario(dockertestMaxWait()) | ||||
| 	assertNoErr(t, err) | ||||
| 	defer scenario.Shutdown() | ||||
| 
 | ||||
| 	spec := map[string]int{ | ||||
| 		"magicdns1": len(MustTestVersions), | ||||
| 		"magicdns2": len(MustTestVersions), | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns")) | ||||
| 	assertNoErrHeadscaleEnv(t, err) | ||||
| 
 | ||||
| 	allClients, err := scenario.ListTailscaleClients() | ||||
| 	assertNoErrListClients(t, err) | ||||
| 
 | ||||
| 	err = scenario.WaitForTailscaleSync() | ||||
| 	assertNoErrSync(t, err) | ||||
| 
 | ||||
| 	// assertClientsState(t, allClients)
 | ||||
| 
 | ||||
| 	// Poor mans cache
 | ||||
| 	_, err = scenario.ListTailscaleClientsFQDNs() | ||||
| 	assertNoErrListFQDN(t, err) | ||||
| 
 | ||||
| 	_, err = scenario.ListTailscaleClientsIPs() | ||||
| 	assertNoErrListClientIPs(t, err) | ||||
| 
 | ||||
| 	for _, client := range allClients { | ||||
| 		for _, peer := range allClients { | ||||
| 			// It is safe to ignore this error as we handled it when caching it
 | ||||
| 			peerFQDN, _ := peer.FQDN() | ||||
| 
 | ||||
| 			assert.Equal(t, fmt.Sprintf("%s.headscale.net", peer.Hostname()), peerFQDN) | ||||
| 
 | ||||
| 			command := []string{ | ||||
| 				"tailscale", | ||||
| 				"ip", peerFQDN, | ||||
| 			} | ||||
| 			result, _, err := client.Execute(command) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf( | ||||
| 					"failed to execute resolve/ip command %s from %s: %s", | ||||
| 					peerFQDN, | ||||
| 					client.Hostname(), | ||||
| 					err, | ||||
| 				) | ||||
| 			} | ||||
| 
 | ||||
| 			ips, err := peer.IPs() | ||||
| 			if err != nil { | ||||
| 				t.Fatalf( | ||||
| 					"failed to get ips for %s: %s", | ||||
| 					peer.Hostname(), | ||||
| 					err, | ||||
| 				) | ||||
| 			} | ||||
| 
 | ||||
| 			for _, ip := range ips { | ||||
| 				if !strings.Contains(result, ip.String()) { | ||||
| 					t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // TestValidateResolvConf validates that the resolv.conf file
 | ||||
| // ends up as expected in our Tailscale containers.
 | ||||
| // All the containers are based on Alpine, meaning Tailscale
 | ||||
| // will overwrite the resolv.conf file.
 | ||||
| // On other platform, Tailscale will integrate with a dns manager
 | ||||
| // if available (like Systemd-Resolved).
 | ||||
| func TestValidateResolvConf(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 
 | ||||
| 	resolvconf := func(conf string) string { | ||||
| 		return strings.ReplaceAll(`# resolv.conf(5) file generated by tailscale | ||||
| # For more info, see https://tailscale.com/s/resolvconf-overwrite
 | ||||
| # DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN | ||||
| `+conf, "\t", "") | ||||
| 	} | ||||
| 
 | ||||
| 	tests := []struct { | ||||
| 		name                string | ||||
| 		conf                map[string]string | ||||
| 		wantConfCompareFunc func(*testing.T, string) | ||||
| 	}{ | ||||
| 		// New config
 | ||||
| 		{ | ||||
| 			name: "no-config", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":        "", | ||||
| 				"HEADSCALE_DNS_MAGIC_DNS":          "false", | ||||
| 				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": "", | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				assert.NotContains(t, got, "100.100.100.100") | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "global-only", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":        "", | ||||
| 				"HEADSCALE_DNS_MAGIC_DNS":          "false", | ||||
| 				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": "8.8.8.8 1.1.1.1", | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				want := resolvconf(` | ||||
| 					nameserver 100.100.100.100 | ||||
| 				`) | ||||
| 				assert.Equal(t, want, got) | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "base-integration-config", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net", | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				want := resolvconf(` | ||||
| 					nameserver 100.100.100.100 | ||||
| 					search very-unique-domain.net | ||||
| 				`) | ||||
| 				assert.Equal(t, want, got) | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "base-magic-dns-off", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_MAGIC_DNS":   "false", | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN": "very-unique-domain.net", | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				want := resolvconf(` | ||||
| 					nameserver 100.100.100.100 | ||||
| 					search very-unique-domain.net | ||||
| 				`) | ||||
| 				assert.Equal(t, want, got) | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "base-extra-search-domains", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_SEARCH_DOMAINS": "test1.no test2.no", | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":    "with-local-dns.net", | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				want := resolvconf(` | ||||
| 					nameserver 100.100.100.100 | ||||
| 					search with-local-dns.net test1.no test2.no | ||||
| 				`) | ||||
| 				assert.Equal(t, want, got) | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "base-nameservers-split", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`, | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":       "with-local-dns.net", | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				want := resolvconf(` | ||||
| 					nameserver 100.100.100.100 | ||||
| 					search with-local-dns.net | ||||
| 				`) | ||||
| 				assert.Equal(t, want, got) | ||||
| 			}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "base-full-no-magic", | ||||
| 			conf: map[string]string{ | ||||
| 				"HEADSCALE_DNS_MAGIC_DNS":          "false", | ||||
| 				"HEADSCALE_DNS_BASE_DOMAIN":        "all-of.it", | ||||
| 				"HEADSCALE_DNS_NAMESERVERS_GLOBAL": `8.8.8.8`, | ||||
| 				"HEADSCALE_DNS_SEARCH_DOMAINS":     "test1.no test2.no", | ||||
| 				// TODO(kradalby): this currently isnt working, need to fix it
 | ||||
| 				// "HEADSCALE_DNS_NAMESERVERS_SPLIT": `{foo.bar.com: ["1.1.1.1"]}`,
 | ||||
| 				// "HEADSCALE_DNS_EXTRA_RECORDS":     `[{ name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.4" }]`,
 | ||||
| 			}, | ||||
| 			wantConfCompareFunc: func(t *testing.T, got string) { | ||||
| 				want := resolvconf(` | ||||
| 					nameserver 100.100.100.100 | ||||
| 					search all-of.it test1.no test2.no | ||||
| 				`) | ||||
| 				assert.Equal(t, want, got) | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			scenario, err := NewScenario(dockertestMaxWait()) | ||||
| 			assertNoErr(t, err) | ||||
| 			defer scenario.Shutdown() | ||||
| 
 | ||||
| 			spec := map[string]int{ | ||||
| 				"resolvconf1": 3, | ||||
| 				"resolvconf2": 3, | ||||
| 			} | ||||
| 
 | ||||
| 			err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("resolvconf"), hsic.WithConfigEnv(tt.conf)) | ||||
| 			assertNoErrHeadscaleEnv(t, err) | ||||
| 
 | ||||
| 			allClients, err := scenario.ListTailscaleClients() | ||||
| 			assertNoErrListClients(t, err) | ||||
| 
 | ||||
| 			err = scenario.WaitForTailscaleSync() | ||||
| 			assertNoErrSync(t, err) | ||||
| 
 | ||||
| 			// Poor mans cache
 | ||||
| 			_, err = scenario.ListTailscaleClientsFQDNs() | ||||
| 			assertNoErrListFQDN(t, err) | ||||
| 
 | ||||
| 			_, err = scenario.ListTailscaleClientsIPs() | ||||
| 			assertNoErrListClientIPs(t, err) | ||||
| 
 | ||||
| 			time.Sleep(30 * time.Second) | ||||
| 
 | ||||
| 			for _, client := range allClients { | ||||
| 				b, err := client.ReadFile("/etc/resolv.conf") | ||||
| 				assertNoErr(t, err) | ||||
| 
 | ||||
| 				t.Logf("comparing resolv conf of %s", client.Hostname()) | ||||
| 				tt.wantConfCompareFunc(t, string(b)) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
| @ -623,74 +623,6 @@ func TestTaildrop(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestResolveMagicDNS(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 	t.Parallel() | ||||
| 
 | ||||
| 	scenario, err := NewScenario(dockertestMaxWait()) | ||||
| 	assertNoErr(t, err) | ||||
| 	defer scenario.Shutdown() | ||||
| 
 | ||||
| 	spec := map[string]int{ | ||||
| 		"magicdns1": len(MustTestVersions), | ||||
| 		"magicdns2": len(MustTestVersions), | ||||
| 	} | ||||
| 
 | ||||
| 	err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns")) | ||||
| 	assertNoErrHeadscaleEnv(t, err) | ||||
| 
 | ||||
| 	allClients, err := scenario.ListTailscaleClients() | ||||
| 	assertNoErrListClients(t, err) | ||||
| 
 | ||||
| 	err = scenario.WaitForTailscaleSync() | ||||
| 	assertNoErrSync(t, err) | ||||
| 
 | ||||
| 	// assertClientsState(t, allClients)
 | ||||
| 
 | ||||
| 	// Poor mans cache
 | ||||
| 	_, err = scenario.ListTailscaleClientsFQDNs() | ||||
| 	assertNoErrListFQDN(t, err) | ||||
| 
 | ||||
| 	_, err = scenario.ListTailscaleClientsIPs() | ||||
| 	assertNoErrListClientIPs(t, err) | ||||
| 
 | ||||
| 	for _, client := range allClients { | ||||
| 		for _, peer := range allClients { | ||||
| 			// It is safe to ignore this error as we handled it when caching it
 | ||||
| 			peerFQDN, _ := peer.FQDN() | ||||
| 
 | ||||
| 			command := []string{ | ||||
| 				"tailscale", | ||||
| 				"ip", peerFQDN, | ||||
| 			} | ||||
| 			result, _, err := client.Execute(command) | ||||
| 			if err != nil { | ||||
| 				t.Fatalf( | ||||
| 					"failed to execute resolve/ip command %s from %s: %s", | ||||
| 					peerFQDN, | ||||
| 					client.Hostname(), | ||||
| 					err, | ||||
| 				) | ||||
| 			} | ||||
| 
 | ||||
| 			ips, err := peer.IPs() | ||||
| 			if err != nil { | ||||
| 				t.Fatalf( | ||||
| 					"failed to get ips for %s: %s", | ||||
| 					peer.Hostname(), | ||||
| 					err, | ||||
| 				) | ||||
| 			} | ||||
| 
 | ||||
| 			for _, ip := range ips { | ||||
| 				if !strings.Contains(result, ip.String()) { | ||||
| 					t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func TestExpireNode(t *testing.T) { | ||||
| 	IntegrationSkip(t) | ||||
| 	t.Parallel() | ||||
|  | ||||
| @ -2,104 +2,6 @@ package hsic | ||||
| 
 | ||||
| import "github.com/juanfont/headscale/hscontrol/types" | ||||
| 
 | ||||
| // const (
 | ||||
| // 	defaultEphemeralNodeInactivityTimeout = time.Second * 30
 | ||||
| // 	defaultNodeUpdateCheckInterval        = time.Second * 10
 | ||||
| // )
 | ||||
| 
 | ||||
| // TODO(kradalby): This approach doesnt work because we cannot
 | ||||
| // serialise our config object to YAML or JSON.
 | ||||
| // func DefaultConfig() headscale.Config {
 | ||||
| // 	derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default")
 | ||||
| //
 | ||||
| // 	config := headscale.Config{
 | ||||
| // 		Log: headscale.LogConfig{
 | ||||
| // 			Level: zerolog.TraceLevel,
 | ||||
| // 		},
 | ||||
| // 		ACL:                            headscale.GetACLConfig(),
 | ||||
| // 		DBtype:                         "sqlite3",
 | ||||
| // 		EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout,
 | ||||
| // 		NodeUpdateCheckInterval:        defaultNodeUpdateCheckInterval,
 | ||||
| // 		IPPrefixes: []netip.Prefix{
 | ||||
| // 			netip.MustParsePrefix("fd7a:115c:a1e0::/48"),
 | ||||
| // 			netip.MustParsePrefix("100.64.0.0/10"),
 | ||||
| // 		},
 | ||||
| // 		DNSConfig: &tailcfg.DNSConfig{
 | ||||
| // 			Proxied: true,
 | ||||
| // 			Nameservers: []netip.Addr{
 | ||||
| // 				netip.MustParseAddr("127.0.0.11"),
 | ||||
| // 				netip.MustParseAddr("1.1.1.1"),
 | ||||
| // 			},
 | ||||
| // 			Resolvers: []*dnstype.Resolver{
 | ||||
| // 				{
 | ||||
| // 					Addr: "127.0.0.11",
 | ||||
| // 				},
 | ||||
| // 				{
 | ||||
| // 					Addr: "1.1.1.1",
 | ||||
| // 				},
 | ||||
| // 			},
 | ||||
| // 		},
 | ||||
| // 		BaseDomain: "headscale.net",
 | ||||
| //
 | ||||
| // 		DBpath: "/tmp/integration_test_db.sqlite3",
 | ||||
| //
 | ||||
| // 		PrivateKeyPath:      "/tmp/integration_private.key",
 | ||||
| // 		NoisePrivateKeyPath: "/tmp/noise_integration_private.key",
 | ||||
| // 		Addr:                "0.0.0.0:8080",
 | ||||
| // 		MetricsAddr:         "127.0.0.1:9090",
 | ||||
| // 		ServerURL:           "http://headscale:8080",
 | ||||
| //
 | ||||
| // 		DERP: headscale.DERPConfig{
 | ||||
| // 			URLs: []url.URL{
 | ||||
| // 				*derpMap,
 | ||||
| // 			},
 | ||||
| // 			AutoUpdate:      false,
 | ||||
| // 			UpdateFrequency: 1 * time.Minute,
 | ||||
| // 		},
 | ||||
| // 	}
 | ||||
| //
 | ||||
| // 	return config
 | ||||
| // }
 | ||||
| 
 | ||||
| // TODO: Reuse the actual configuration object above.
 | ||||
| // Deprecated: use env function instead as it is easier to
 | ||||
| // override.
 | ||||
| func DefaultConfigYAML() string { | ||||
| 	yaml := ` | ||||
| log: | ||||
|   level: trace | ||||
| acl_policy_path: "" | ||||
| database: | ||||
|   type: sqlite3 | ||||
|   sqlite.path: /tmp/integration_test_db.sqlite3 | ||||
| ephemeral_node_inactivity_timeout: 30m | ||||
| prefixes: | ||||
|   v6: fd7a:115c:a1e0::/48 | ||||
|   v4: 100.64.0.0/10 | ||||
| dns_config: | ||||
|   base_domain: headscale.net | ||||
|   magic_dns: true | ||||
|   domains: [] | ||||
|   nameservers: | ||||
|     - 127.0.0.11 | ||||
|     - 1.1.1.1 | ||||
| private_key_path: /tmp/private.key | ||||
| noise: | ||||
|   private_key_path: /tmp/noise_private.key | ||||
| listen_addr: 0.0.0.0:8080 | ||||
| metrics_listen_addr: 127.0.0.1:9090 | ||||
| server_url: http://headscale:8080
 | ||||
| 
 | ||||
| derp: | ||||
|   urls: | ||||
|     - https://controlplane.tailscale.com/derpmap/default
 | ||||
|   auto_update_enabled: false | ||||
|   update_frequency: 1m | ||||
| ` | ||||
| 
 | ||||
| 	return yaml | ||||
| } | ||||
| 
 | ||||
| func MinimumConfigYAML() string { | ||||
| 	return ` | ||||
| private_key_path: /tmp/private.key | ||||
| @ -117,10 +19,9 @@ func DefaultConfigEnv() map[string]string { | ||||
| 		"HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m", | ||||
| 		"HEADSCALE_PREFIXES_V4":                       "100.64.0.0/10", | ||||
| 		"HEADSCALE_PREFIXES_V6":                       "fd7a:115c:a1e0::/48", | ||||
| 		"HEADSCALE_DNS_CONFIG_BASE_DOMAIN":            "headscale.net", | ||||
| 		"HEADSCALE_DNS_CONFIG_MAGIC_DNS":              "true", | ||||
| 		"HEADSCALE_DNS_CONFIG_DOMAINS":                "", | ||||
| 		"HEADSCALE_DNS_CONFIG_NAMESERVERS":            "127.0.0.11 1.1.1.1", | ||||
| 		"HEADSCALE_DNS_BASE_DOMAIN":                   "headscale.net", | ||||
| 		"HEADSCALE_DNS_MAGIC_DNS":                     "true", | ||||
| 		"HEADSCALE_DNS_NAMESERVERS_GLOBAL":            "127.0.0.11 1.1.1.1", | ||||
| 		"HEADSCALE_PRIVATE_KEY_PATH":                  "/tmp/private.key", | ||||
| 		"HEADSCALE_NOISE_PRIVATE_KEY_PATH":            "/tmp/noise_private.key", | ||||
| 		"HEADSCALE_LISTEN_ADDR":                       "0.0.0.0:8080", | ||||
|  | ||||
| @ -51,6 +51,8 @@ var ( | ||||
| 	tailscaleVersions2021 = map[string]bool{ | ||||
| 		"head":     true, | ||||
| 		"unstable": true, | ||||
| 		"1.70":     true,  // CapVer: not checked
 | ||||
| 		"1.68":     true,  // CapVer: not checked
 | ||||
| 		"1.66":     true,  // CapVer: not checked
 | ||||
| 		"1.64":     true,  // CapVer: not checked
 | ||||
| 		"1.62":     true,  // CapVer: not checked
 | ||||
| @ -62,10 +64,10 @@ var ( | ||||
| 		"1.50":     true,  // CapVer: 74
 | ||||
| 		"1.48":     true,  // CapVer: 68
 | ||||
| 		"1.46":     true,  // CapVer: 65
 | ||||
| 		"1.44":     true,  // CapVer: 63
 | ||||
| 		"1.42":     true,  // CapVer: 61
 | ||||
| 		"1.40":     true,  // CapVer: 61
 | ||||
| 		"1.38":     true,  // Oldest supported version, CapVer: 58
 | ||||
| 		"1.44":     false, // CapVer: 63
 | ||||
| 		"1.42":     false, // Oldest supported version, CapVer: 61
 | ||||
| 		"1.40":     false, // CapVer: 61
 | ||||
| 		"1.38":     false, // CapVer: 58
 | ||||
| 		"1.36":     false, // CapVer: 56
 | ||||
| 		"1.34":     false, // CapVer: 51
 | ||||
| 		"1.32":     false, // CapVer: 46
 | ||||
|  | ||||
| @ -36,6 +36,7 @@ type TailscaleClient interface { | ||||
| 	Ping(hostnameOrIP string, opts ...tsic.PingOption) error | ||||
| 	Curl(url string, opts ...tsic.CurlOption) (string, error) | ||||
| 	ID() string | ||||
| 	ReadFile(path string) ([]byte, error) | ||||
| 
 | ||||
| 	// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client
 | ||||
| 	// and a bool indicating if the clients online count and peer count is equal.
 | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| package tsic | ||||
| 
 | ||||
| import ( | ||||
| 	"archive/tar" | ||||
| 	"bytes" | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| @ -998,3 +1000,41 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { | ||||
| func (t *TailscaleInContainer) SaveLog(path string) error { | ||||
| 	return dockertestutil.SaveLog(t.pool, t.container, path) | ||||
| } | ||||
| 
 | ||||
| // ReadFile reads a file from the Tailscale container.
 | ||||
| // It returns the content of the file as a byte slice.
 | ||||
| func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) { | ||||
| 	tarBytes, err := integrationutil.FetchPathFromContainer(t.pool, t.container, path) | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("reading file from container: %w", err) | ||||
| 	} | ||||
| 
 | ||||
| 	var out bytes.Buffer | ||||
| 	tr := tar.NewReader(bytes.NewReader(tarBytes)) | ||||
| 	for { | ||||
| 		hdr, err := tr.Next() | ||||
| 		if err == io.EOF { | ||||
| 			break // End of archive
 | ||||
| 		} | ||||
| 		if err != nil { | ||||
| 			return nil, fmt.Errorf("reading tar header: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		if !strings.Contains(path, hdr.Name) { | ||||
| 			return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name) | ||||
| 		} | ||||
| 
 | ||||
| 		if _, err := io.Copy(&out, tr); err != nil { | ||||
| 			return nil, fmt.Errorf("copying file to buffer: %w", err) | ||||
| 		} | ||||
| 
 | ||||
| 		// Only support reading the first tile
 | ||||
| 		break | ||||
| 	} | ||||
| 
 | ||||
| 	if out.Len() == 0 { | ||||
| 		return nil, fmt.Errorf("file is empty") | ||||
| 	} | ||||
| 
 | ||||
| 	return out.Bytes(), nil | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user