mirror of
https://github.com/juanfont/headscale.git
synced 2024-11-29 18:33:05 +00: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
25 changed files with 1036 additions and 453 deletions
3
.github/workflows/test-integration.yaml
vendored
3
.github/workflows/test-integration.yaml
vendored
|
@ -37,6 +37,8 @@ jobs:
|
||||||
- TestNodeRenameCommand
|
- TestNodeRenameCommand
|
||||||
- TestNodeMoveCommand
|
- TestNodeMoveCommand
|
||||||
- TestPolicyCommand
|
- TestPolicyCommand
|
||||||
|
- TestResolveMagicDNS
|
||||||
|
- TestValidateResolvConf
|
||||||
- TestDERPServerScenario
|
- TestDERPServerScenario
|
||||||
- TestPingAllByIP
|
- TestPingAllByIP
|
||||||
- TestPingAllByIPPublicDERP
|
- TestPingAllByIPPublicDERP
|
||||||
|
@ -45,7 +47,6 @@ jobs:
|
||||||
- TestEphemeral2006DeletedTooQuickly
|
- TestEphemeral2006DeletedTooQuickly
|
||||||
- TestPingAllByHostname
|
- TestPingAllByHostname
|
||||||
- TestTaildrop
|
- TestTaildrop
|
||||||
- TestResolveMagicDNS
|
|
||||||
- TestExpireNode
|
- TestExpireNode
|
||||||
- TestNodeOnlineStatus
|
- TestNodeOnlineStatus
|
||||||
- TestPingAllByIPManyUpDown
|
- 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.
|
- 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)
|
- 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)
|
- 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)
|
- 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.
|
- 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)
|
- 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 []()
|
- 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.
|
- 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.
|
- `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.
|
||||||
|
- dns.base_domain can no longer be the same as (or part of) server_url.
|
||||||
- This option brings Headscales behaviour in line with Tailscale.
|
- 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)
|
- 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.
|
- 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
|
### 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_hostname"), check.Equals, "")
|
||||||
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
|
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.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(
|
c.Assert(
|
||||||
util.GetFileMode("unix_socket_permission"),
|
util.GetFileMode("unix_socket_permission"),
|
||||||
check.Equals,
|
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_hostname"), check.Equals, "")
|
||||||
c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http")
|
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.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(
|
c.Assert(
|
||||||
util.GetFileMode("unix_socket_permission"),
|
util.GetFileMode("unix_socket_permission"),
|
||||||
check.Equals,
|
check.Equals,
|
||||||
|
@ -116,39 +114,6 @@ func (*Suite) TestConfigLoading(c *check.C) {
|
||||||
c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false)
|
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) {
|
func writeConfig(c *check.C, tmpDir string, configYaml []byte) {
|
||||||
// Populate a custom config file
|
// Populate a custom config file
|
||||||
configFile := filepath.Join(tmpDir, "config.yaml")
|
configFile := filepath.Join(tmpDir, "config.yaml")
|
||||||
|
|
|
@ -224,43 +224,60 @@ policy:
|
||||||
# - https://tailscale.com/kb/1081/magicdns/
|
# - https://tailscale.com/kb/1081/magicdns/
|
||||||
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
|
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
|
||||||
#
|
#
|
||||||
dns_config:
|
# Please not that for the DNS configuration to have any effect,
|
||||||
# Whether to prefer using Headscale provided DNS or use local.
|
# clients must have the `--accept-ds=true` option enabled. This is the
|
||||||
override_local_dns: true
|
# 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.
|
# List of DNS servers to expose to clients.
|
||||||
nameservers:
|
nameservers:
|
||||||
|
global:
|
||||||
- 1.1.1.1
|
- 1.1.1.1
|
||||||
|
- 1.0.0.1
|
||||||
|
- 2606:4700:4700::1111
|
||||||
|
- 2606:4700:4700::1001
|
||||||
|
|
||||||
# NextDNS (see https://tailscale.com/kb/1218/nextdns/).
|
# NextDNS (see https://tailscale.com/kb/1218/nextdns/).
|
||||||
# "abc123" is example NextDNS ID, replace with yours.
|
# "abc123" is example NextDNS ID, replace with yours.
|
||||||
#
|
|
||||||
# With metadata sharing:
|
|
||||||
# nameservers:
|
|
||||||
# - https://dns.nextdns.io/abc123
|
# - https://dns.nextdns.io/abc123
|
||||||
#
|
|
||||||
# Without metadata sharing:
|
|
||||||
# nameservers:
|
|
||||||
# - 2a07:a8c0::ab:c123
|
|
||||||
# - 2a07:a8c1::ab:c123
|
|
||||||
|
|
||||||
# Split DNS (see https://tailscale.com/kb/1054/dns/),
|
# Split DNS (see https://tailscale.com/kb/1054/dns/),
|
||||||
# list of search domains and the DNS to query for each one.
|
# a map of domains and which DNS server to use for each.
|
||||||
#
|
split:
|
||||||
# restricted_nameservers:
|
{}
|
||||||
# foo.bar.com:
|
# foo.bar.com:
|
||||||
# - 1.1.1.1
|
# - 1.1.1.1
|
||||||
# darp.headscale.net:
|
# darp.headscale.net:
|
||||||
# - 1.1.1.1
|
# - 1.1.1.1
|
||||||
# - 8.8.8.8
|
# - 8.8.8.8
|
||||||
|
|
||||||
# Search domains to inject.
|
# Set custom DNS search domains. With MagicDNS enabled,
|
||||||
domains: []
|
# your tailnet base_domain is always the first search domain.
|
||||||
|
search_domains: []
|
||||||
|
|
||||||
# Extra DNS records
|
# Extra DNS records
|
||||||
# so far only A-records are supported (on the tailscale side)
|
# so far only A-records are supported (on the tailscale side)
|
||||||
# See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
|
# See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
|
||||||
# extra_records:
|
extra_records: []
|
||||||
# - name: "grafana.myvpn.example.com"
|
# - name: "grafana.myvpn.example.com"
|
||||||
# type: "A"
|
# type: "A"
|
||||||
# value: "100.64.0.3"
|
# value: "100.64.0.3"
|
||||||
|
@ -268,10 +285,6 @@ dns_config:
|
||||||
# # you can also put it in one line
|
# # you can also put it in one line
|
||||||
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
|
# - { 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
|
# DEPRECATED
|
||||||
# Use the username as part of the DNS name for nodes, with this option enabled:
|
# Use the username as part of the DNS name for nodes, with this option enabled:
|
||||||
# node1.username.example.com
|
# node1.username.example.com
|
||||||
|
@ -281,12 +294,6 @@ dns_config:
|
||||||
# while in upstream Tailscale, the username is not included.
|
# while in upstream Tailscale, the username is not included.
|
||||||
use_username_in_magic_dns: false
|
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
|
# Unix socket used for the CLI to connect without authentication
|
||||||
# Note: for production you will want to set this to something like:
|
# Note: for production you will want to set this to something like:
|
||||||
unix_socket: /var/run/headscale/headscale.sock
|
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,
|
# 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.
|
# 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"];
|
subPackages = ["cmd/headscale"];
|
||||||
|
|
||||||
|
|
25
go.mod
25
go.mod
|
@ -31,15 +31,15 @@ require (
|
||||||
github.com/samber/lo v1.39.0
|
github.com/samber/lo v1.39.0
|
||||||
github.com/sasha-s/go-deadlock v0.3.1
|
github.com/sasha-s/go-deadlock v0.3.1
|
||||||
github.com/spf13/cobra v1.8.0
|
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/stretchr/testify v1.9.0
|
||||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
|
||||||
github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1
|
github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1
|
||||||
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
golang.org/x/crypto v0.23.0
|
golang.org/x/crypto v0.25.0
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
|
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||||
golang.org/x/net v0.25.0
|
golang.org/x/net v0.27.0
|
||||||
golang.org/x/oauth2 v0.20.0
|
golang.org/x/oauth2 v0.20.0
|
||||||
golang.org/x/sync v0.7.0
|
golang.org/x/sync v0.7.0
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291
|
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-jose/go-jose/v4 v4.0.1 // indirect
|
||||||
github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // 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-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/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect
|
||||||
github.com/gogo/protobuf v1.3.2 // indirect
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1 // 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/csrf v1.7.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/hashicorp/go-version v1.6.0 // 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/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
github.com/illarion/gonotify v1.0.1 // indirect
|
github.com/illarion/gonotify v1.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
@ -137,7 +137,6 @@ require (
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/lib/pq v1.10.7 // indirect
|
github.com/lib/pq v1.10.7 // indirect
|
||||||
github.com/lithammer/fuzzysearch v1.1.8 // 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-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // 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/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
github.com/safchain/ethtool v0.3.0 // indirect
|
github.com/safchain/ethtool v0.3.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
github.com/spf13/afero v1.11.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
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.19.0 // indirect
|
||||||
golang.org/x/sys v0.20.0 // indirect
|
golang.org/x/sys v0.22.0 // indirect
|
||||||
golang.org/x/term v0.20.0 // indirect
|
golang.org/x/term v0.22.0 // indirect
|
||||||
golang.org/x/text v0.15.0 // indirect
|
golang.org/x/text v0.16.0 // indirect
|
||||||
golang.org/x/time v0.5.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/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // 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
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect
|
gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect
|
||||||
modernc.org/libc v1.50.6 // 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 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
|
||||||
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
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-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/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/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
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/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 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
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 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
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 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||||
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
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=
|
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/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 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
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/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.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
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/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 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
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.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||||
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/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
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=
|
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/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 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
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.20.0-alpha.6 h1:f65Cr/+2qk4GfHC0xqT/isoupQppwN5+VLRztUGTDbY=
|
||||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
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-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.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.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
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-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-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||||
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
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 h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM=
|
||||||
golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
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=
|
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.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.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.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
|
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
|
||||||
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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.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.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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
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.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 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||||
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.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.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
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-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-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
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.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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
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.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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.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.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.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.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
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 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
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=
|
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.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.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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||||
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
|
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
|
||||||
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
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-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-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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-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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
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.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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.8/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
|
return tNodes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tailNode converts a Node into a Tailscale Node. includeRoutes is false for shared nodes
|
// tailNode converts a Node into a Tailscale Node.
|
||||||
// as per the expected behaviour in the official SaaS.
|
|
||||||
func tailNode(
|
func tailNode(
|
||||||
node *types.Node,
|
node *types.Node,
|
||||||
capVer tailcfg.CapabilityVersion,
|
capVer tailcfg.CapabilityVersion,
|
||||||
|
|
|
@ -55,12 +55,14 @@ func TestTailNode(t *testing.T) {
|
||||||
{
|
{
|
||||||
name: "empty-node",
|
name: "empty-node",
|
||||||
node: &types.Node{
|
node: &types.Node{
|
||||||
|
GivenName: "empty",
|
||||||
Hostinfo: &tailcfg.Hostinfo{},
|
Hostinfo: &tailcfg.Hostinfo{},
|
||||||
},
|
},
|
||||||
pol: &policy.ACLPolicy{},
|
pol: &policy.ACLPolicy{},
|
||||||
dnsConfig: &tailcfg.DNSConfig{},
|
dnsConfig: &tailcfg.DNSConfig{},
|
||||||
baseDomain: "",
|
baseDomain: "",
|
||||||
want: &tailcfg.Node{
|
want: &tailcfg.Node{
|
||||||
|
Name: "empty",
|
||||||
StableID: "0",
|
StableID: "0",
|
||||||
Addresses: []netip.Prefix{},
|
Addresses: []netip.Prefix{},
|
||||||
AllowedIPs: []netip.Prefix{},
|
AllowedIPs: []netip.Prefix{},
|
||||||
|
|
|
@ -166,7 +166,7 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
MinimumCapVersion tailcfg.CapabilityVersion = 58
|
MinimumCapVersion tailcfg.CapabilityVersion = 61
|
||||||
)
|
)
|
||||||
|
|
||||||
// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
|
// NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol
|
||||||
|
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/types/dnstype"
|
"tailscale.com/types/dnstype"
|
||||||
|
"tailscale.com/util/set"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -88,6 +89,20 @@ type Config struct {
|
||||||
Tuning Tuning
|
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 {
|
type SqliteConfig struct {
|
||||||
Path string
|
Path string
|
||||||
WriteAheadLog bool
|
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.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
|
@ -213,9 +229,13 @@ func LoadConfig(path string, isFile bool) error {
|
||||||
viper.SetDefault("log.level", "info")
|
viper.SetDefault("log.level", "info")
|
||||||
viper.SetDefault("log.format", TextLogFormat)
|
viper.SetDefault("log.format", TextLogFormat)
|
||||||
|
|
||||||
viper.SetDefault("dns_config", nil)
|
viper.SetDefault("dns.magic_dns", true)
|
||||||
viper.SetDefault("dns_config.override_local_dns", true)
|
viper.SetDefault("dns.base_domain", "")
|
||||||
viper.SetDefault("dns_config.use_username_in_magic_dns", false)
|
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.enabled", false)
|
||||||
viper.SetDefault("derp.server.stun.enabled", true)
|
viper.SetDefault("derp.server.stun.enabled", true)
|
||||||
|
@ -259,17 +279,33 @@ func LoadConfig(path string, isFile bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
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)
|
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
|
// Register aliases for backward compatibility
|
||||||
// Has to be called _after_ viper.ReadInConfig()
|
// Has to be called _after_ viper.ReadInConfig()
|
||||||
// https://github.com/spf13/viper/issues/560
|
// https://github.com/spf13/viper/issues/560
|
||||||
|
|
||||||
// Alias the old ACL Policy path with the new configuration option.
|
// 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
|
// Collect any validation errors and return them all at once
|
||||||
var errorText string
|
var errorText string
|
||||||
|
@ -485,123 +521,131 @@ func GetDatabaseConfig() DatabaseConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDNSConfig() (*tailcfg.DNSConfig, string) {
|
func DNS() (DNSConfig, error) {
|
||||||
if viper.IsSet("dns_config") {
|
var dns DNSConfig
|
||||||
dnsConfig := &tailcfg.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") {
|
dns.MagicDNS = viper.GetBool("dns.magic_dns")
|
||||||
nameserversStr := viper.GetStringSlice("dns_config.nameservers")
|
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{}
|
if viper.IsSet("dns.extra_records") {
|
||||||
resolvers := []*dnstype.Resolver{}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
var extraRecords []tailcfg.DNSRecord
|
||||||
|
|
||||||
err := viper.UnmarshalKey("dns_config.extra_records", &extraRecords)
|
err := viper.UnmarshalKey("dns.extra_records", &extraRecords)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error().
|
return DNSConfig{}, fmt.Errorf("unmarshaling dns extra records: %w", err)
|
||||||
Str("func", "getDNSConfig").
|
|
||||||
Err(err).
|
|
||||||
Msgf("Could not parse dns_config.extra_records")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsConfig.ExtraRecords = extraRecords
|
dns.ExtraRecords = extraRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
if viper.IsSet("dns_config.magic_dns") {
|
dns.UserNameInMagicDNS = viper.GetBool("dns.use_username_in_magic_dns")
|
||||||
dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns")
|
|
||||||
}
|
|
||||||
|
|
||||||
var baseDomain string
|
return dns, nil
|
||||||
if viper.IsSet("dns_config.base_domain") {
|
}
|
||||||
baseDomain = viper.GetString("dns_config.base_domain")
|
|
||||||
|
// 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 {
|
} else {
|
||||||
baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled
|
warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !viper.GetBool("dns_config.use_username_in_magic_dns") {
|
if _, err := url.Parse(nsStr); err == nil {
|
||||||
dnsConfig.Domains = []string{baseDomain}
|
resolvers = append(resolvers, &dnstype.Resolver{
|
||||||
|
Addr: nsStr,
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
log.Warn().Msg("DNS: Usernames in DNS has been deprecated, this option will be remove in future versions")
|
warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err)
|
||||||
log.Warn().Msg("DNS: see 0.23.0 changelog for more information.")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if domains := viper.GetStringSlice("dns_config.domains"); len(domains) > 0 {
|
if warn != "" {
|
||||||
dnsConfig.Domains = append(dnsConfig.Domains, domains...)
|
log.Warn().Msg(warn)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded")
|
return resolvers
|
||||||
return dnsConfig, baseDomain
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, ""
|
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) {
|
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)
|
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()
|
derpConfig := GetDERPConfig()
|
||||||
logTailConfig := GetLogTailConfig()
|
logTailConfig := GetLogTailConfig()
|
||||||
randomizeClientPort := viper.GetBool("randomize_client_port")
|
randomizeClientPort := viper.GetBool("randomize_client_port")
|
||||||
|
@ -711,8 +759,23 @@ func GetHeadscaleConfig() (*Config, error) {
|
||||||
oidcClientSecret = strings.TrimSpace(string(secretBytes))
|
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{
|
return &Config{
|
||||||
ServerURL: viper.GetString("server_url"),
|
ServerURL: serverURL,
|
||||||
Addr: viper.GetString("listen_addr"),
|
Addr: viper.GetString("listen_addr"),
|
||||||
MetricsAddr: viper.GetString("metrics_listen_addr"),
|
MetricsAddr: viper.GetString("metrics_listen_addr"),
|
||||||
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
GRPCAddr: viper.GetString("grpc_listen_addr"),
|
||||||
|
@ -726,7 +789,7 @@ func GetHeadscaleConfig() (*Config, error) {
|
||||||
NoisePrivateKeyPath: util.AbsolutePathFromConfigPath(
|
NoisePrivateKeyPath: util.AbsolutePathFromConfigPath(
|
||||||
viper.GetString("noise.private_key_path"),
|
viper.GetString("noise.private_key_path"),
|
||||||
),
|
),
|
||||||
BaseDomain: baseDomain,
|
BaseDomain: dnsConfig.BaseDomain,
|
||||||
|
|
||||||
DERP: derpConfig,
|
DERP: derpConfig,
|
||||||
|
|
||||||
|
@ -738,8 +801,8 @@ func GetHeadscaleConfig() (*Config, error) {
|
||||||
|
|
||||||
TLS: GetTLSConfig(),
|
TLS: GetTLSConfig(),
|
||||||
|
|
||||||
DNSConfig: dnsConfig,
|
DNSConfig: DNSToTailcfgDNS(dnsConfig),
|
||||||
DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"),
|
DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS,
|
||||||
|
|
||||||
ACMEEmail: viper.GetString("acme_email"),
|
ACMEEmail: viper.GetString("acme_email"),
|
||||||
ACMEURL: viper.GetString("acme_url"),
|
ACMEURL: viper.GetString("acme_url"),
|
||||||
|
@ -805,19 +868,70 @@ func IsCLIConfigured() bool {
|
||||||
return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != ""
|
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.
|
// 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
|
// NOTE: RegisterAlias is called with NEW KEY -> OLD KEY
|
||||||
viper.RegisterAlias(newKey, oldKey)
|
viper.RegisterAlias(newKey, oldKey)
|
||||||
if viper.IsSet(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.
|
// fatal deprecates and adds an entry to the fatal list of options if the oldKey is set.
|
||||||
func deprecateAndFatal(newKey, oldKey string) {
|
func (d *deprecator) fatal(newKey, oldKey string) {
|
||||||
if viper.IsSet(oldKey) {
|
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,17 +394,19 @@ func (node *Node) Proto() *v1.Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) {
|
func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) {
|
||||||
var hostname string
|
|
||||||
if cfg.DNSConfig != nil && cfg.DNSConfig.Proxied { // MagicDNS
|
|
||||||
if node.GivenName == "" {
|
if node.GivenName == "" {
|
||||||
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName)
|
return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hostname := node.GivenName
|
||||||
|
|
||||||
|
if baseDomain != "" {
|
||||||
hostname = fmt.Sprintf(
|
hostname = fmt.Sprintf(
|
||||||
"%s.%s",
|
"%s.%s",
|
||||||
node.GivenName,
|
node.GivenName,
|
||||||
baseDomain,
|
baseDomain,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.DNSUserNameInMagicDNS {
|
if cfg.DNSUserNameInMagicDNS {
|
||||||
if node.User.Name == "" {
|
if node.User.Name == "" {
|
||||||
|
@ -426,9 +428,6 @@ func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) {
|
||||||
ErrHostnameTooLong,
|
ErrHostnameTooLong,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
hostname = node.GivenName
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostname, nil
|
return hostname, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -195,7 +195,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||||
DNSUserNameInMagicDNS: true,
|
DNSUserNameInMagicDNS: true,
|
||||||
},
|
},
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
want: "test",
|
want: "test.user.example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no-dnsconfig-with-username",
|
name: "no-dnsconfig-with-username",
|
||||||
|
@ -206,7 +206,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
want: "test",
|
want: "test.example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "all-set",
|
name: "all-set",
|
||||||
|
@ -271,7 +271,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||||
DNSUserNameInMagicDNS: false,
|
DNSUserNameInMagicDNS: false,
|
||||||
},
|
},
|
||||||
domain: "example.com",
|
domain: "example.com",
|
||||||
want: "test",
|
want: "test.example.com",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "no-dnsconfig",
|
name: "no-dnsconfig",
|
||||||
|
@ -282,7 +282,7 @@ func TestNodeFQDN(t *testing.T) {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
domain: "example.com",
|
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) {
|
func TestExpireNode(t *testing.T) {
|
||||||
IntegrationSkip(t)
|
IntegrationSkip(t)
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
|
@ -2,104 +2,6 @@ package hsic
|
||||||
|
|
||||||
import "github.com/juanfont/headscale/hscontrol/types"
|
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 {
|
func MinimumConfigYAML() string {
|
||||||
return `
|
return `
|
||||||
private_key_path: /tmp/private.key
|
private_key_path: /tmp/private.key
|
||||||
|
@ -117,10 +19,9 @@ func DefaultConfigEnv() map[string]string {
|
||||||
"HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m",
|
"HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m",
|
||||||
"HEADSCALE_PREFIXES_V4": "100.64.0.0/10",
|
"HEADSCALE_PREFIXES_V4": "100.64.0.0/10",
|
||||||
"HEADSCALE_PREFIXES_V6": "fd7a:115c:a1e0::/48",
|
"HEADSCALE_PREFIXES_V6": "fd7a:115c:a1e0::/48",
|
||||||
"HEADSCALE_DNS_CONFIG_BASE_DOMAIN": "headscale.net",
|
"HEADSCALE_DNS_BASE_DOMAIN": "headscale.net",
|
||||||
"HEADSCALE_DNS_CONFIG_MAGIC_DNS": "true",
|
"HEADSCALE_DNS_MAGIC_DNS": "true",
|
||||||
"HEADSCALE_DNS_CONFIG_DOMAINS": "",
|
"HEADSCALE_DNS_NAMESERVERS_GLOBAL": "127.0.0.11 1.1.1.1",
|
||||||
"HEADSCALE_DNS_CONFIG_NAMESERVERS": "127.0.0.11 1.1.1.1",
|
|
||||||
"HEADSCALE_PRIVATE_KEY_PATH": "/tmp/private.key",
|
"HEADSCALE_PRIVATE_KEY_PATH": "/tmp/private.key",
|
||||||
"HEADSCALE_NOISE_PRIVATE_KEY_PATH": "/tmp/noise_private.key",
|
"HEADSCALE_NOISE_PRIVATE_KEY_PATH": "/tmp/noise_private.key",
|
||||||
"HEADSCALE_LISTEN_ADDR": "0.0.0.0:8080",
|
"HEADSCALE_LISTEN_ADDR": "0.0.0.0:8080",
|
||||||
|
|
|
@ -51,6 +51,8 @@ var (
|
||||||
tailscaleVersions2021 = map[string]bool{
|
tailscaleVersions2021 = map[string]bool{
|
||||||
"head": true,
|
"head": true,
|
||||||
"unstable": true,
|
"unstable": true,
|
||||||
|
"1.70": true, // CapVer: not checked
|
||||||
|
"1.68": true, // CapVer: not checked
|
||||||
"1.66": true, // CapVer: not checked
|
"1.66": true, // CapVer: not checked
|
||||||
"1.64": true, // CapVer: not checked
|
"1.64": true, // CapVer: not checked
|
||||||
"1.62": true, // CapVer: not checked
|
"1.62": true, // CapVer: not checked
|
||||||
|
@ -62,10 +64,10 @@ var (
|
||||||
"1.50": true, // CapVer: 74
|
"1.50": true, // CapVer: 74
|
||||||
"1.48": true, // CapVer: 68
|
"1.48": true, // CapVer: 68
|
||||||
"1.46": true, // CapVer: 65
|
"1.46": true, // CapVer: 65
|
||||||
"1.44": true, // CapVer: 63
|
"1.44": false, // CapVer: 63
|
||||||
"1.42": true, // CapVer: 61
|
"1.42": false, // Oldest supported version, CapVer: 61
|
||||||
"1.40": true, // CapVer: 61
|
"1.40": false, // CapVer: 61
|
||||||
"1.38": true, // Oldest supported version, CapVer: 58
|
"1.38": false, // CapVer: 58
|
||||||
"1.36": false, // CapVer: 56
|
"1.36": false, // CapVer: 56
|
||||||
"1.34": false, // CapVer: 51
|
"1.34": false, // CapVer: 51
|
||||||
"1.32": false, // CapVer: 46
|
"1.32": false, // CapVer: 46
|
||||||
|
|
|
@ -36,6 +36,7 @@ type TailscaleClient interface {
|
||||||
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
|
Ping(hostnameOrIP string, opts ...tsic.PingOption) error
|
||||||
Curl(url string, opts ...tsic.CurlOption) (string, error)
|
Curl(url string, opts ...tsic.CurlOption) (string, error)
|
||||||
ID() string
|
ID() string
|
||||||
|
ReadFile(path string) ([]byte, error)
|
||||||
|
|
||||||
// FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client
|
// 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.
|
// and a bool indicating if the clients online count and peer count is equal.
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package tsic
|
package tsic
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -998,3 +1000,41 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error {
|
||||||
func (t *TailscaleInContainer) SaveLog(path string) error {
|
func (t *TailscaleInContainer) SaveLog(path string) error {
|
||||||
return dockertestutil.SaveLog(t.pool, t.container, path)
|
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 a new issue