From 948d53f934b83f0ca6d4d5007973b334a4ed306a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 4 Aug 2024 06:35:46 +0000 Subject: [PATCH 01/60] flake.lock: Update (#2042) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index ec02aa37..7e855a25 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722073938, - "narHash": "sha256-OpX0StkL8vpXyWOGUD6G+MA26wAXK6SpT94kLJXo6B4=", + "lastModified": 1722640603, + "narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "e36e9f57337d0ff0cf77aceb58af4c805472bfae", + "rev": "81610abc161d4021b29199aa464d6a1a521e0cc9", "type": "github" }, "original": { From ece907d878fc6fd08085fbed796eba35dfe103fa Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 11 Aug 2024 07:44:59 +0200 Subject: [PATCH 02/60] test embedded derp with derp updater, check client health (#2030) --- hscontrol/derp/derp.go | 5 --- integration/embedded_derp_test.go | 64 ++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/hscontrol/derp/derp.go b/hscontrol/derp/derp.go index 3afcb4ea..5d4b24f2 100644 --- a/hscontrol/derp/derp.go +++ b/hscontrol/derp/derp.go @@ -125,10 +125,5 @@ func GetDERPMap(cfg types.DERPConfig) *tailcfg.DERPMap { log.Trace().Interface("derpMap", derpMap).Msg("DERPMap loaded") - if len(derpMap.Regions) == 0 { - log.Warn(). - Msg("DERP map is empty, not a single DERP map datasource was loaded correctly or contained a region") - } - return derpMap } diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 39a9acca..745f2c89 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -4,7 +4,9 @@ import ( "fmt" "log" "net/url" + "strings" "testing" + "time" "github.com/juanfont/headscale/hscontrol/util" "github.com/juanfont/headscale/integration/dockertestutil" @@ -33,8 +35,7 @@ func TestDERPServerScenario(t *testing.T) { defer scenario.Shutdown() spec := map[string]int{ - "user1": 10, - // "user1": len(MustTestVersions), + "user1": len(MustTestVersions), } err = scenario.CreateHeadscaleEnv( @@ -44,24 +45,75 @@ func TestDERPServerScenario(t *testing.T) { hsic.WithEmbeddedDERPServerOnly(), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "true", + "HEADSCALE_DERP_UPDATE_FREQUENCY": "10s", + }), ) assertNoErrHeadscaleEnv(t, err) allClients, err := scenario.ListTailscaleClients() assertNoErrListClients(t, err) - allIps, err := scenario.ListTailscaleClientsIPs() - assertNoErrListClientIPs(t, err) - err = scenario.WaitForTailscaleSync() assertNoErrSync(t, err) allHostnames, err := scenario.ListTailscaleClientsFQDNs() assertNoErrListFQDN(t, err) + for _, client := range allClients { + status, err := client.Status() + assertNoErr(t, err) + + for _, health := range status.Health { + if strings.Contains(health, "could not connect to any relay server") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + if strings.Contains(health, "could not connect to the 'Headscale Embedded DERP' relay server.") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + } + } + success := pingDerpAllHelper(t, allClients, allHostnames) - t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + for _, client := range allClients { + status, err := client.Status() + assertNoErr(t, err) + + for _, health := range status.Health { + if strings.Contains(health, "could not connect to any relay server") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + if strings.Contains(health, "could not connect to the 'Headscale Embedded DERP' relay server.") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + } + } + + t.Logf("Run 1: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) + + // Let the DERP updater run a couple of times to ensure it does not + // break the DERPMap. + time.Sleep(30 * time.Second) + + success = pingDerpAllHelper(t, allClients, allHostnames) + + for _, client := range allClients { + status, err := client.Status() + assertNoErr(t, err) + + for _, health := range status.Health { + if strings.Contains(health, "could not connect to any relay server") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + if strings.Contains(health, "could not connect to the 'Headscale Embedded DERP' relay server.") { + t.Errorf("expected to be connected to derp, found: %s", health) + } + } + } + + t.Logf("Run2: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) } func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( From fcd1183805df3da6f8fe5bec79edf970015c63b9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 11 Aug 2024 05:48:15 +0000 Subject: [PATCH 03/60] flake.lock: Update (#2052) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 7e855a25..627b7598 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1722640603, - "narHash": "sha256-TcXjLVNd3VeH1qKPH335Tc4RbFDbZQX+d7rqnDUoRaY=", + "lastModified": 1723221148, + "narHash": "sha256-7pjpeQlZUNQ4eeVntytU3jkw9dFK3k1Htgk2iuXjaD8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "81610abc161d4021b29199aa464d6a1a521e0cc9", + "rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6", "type": "github" }, "original": { From 022fb24cd92035470496d50d86bf8c9ee74b1e7e Mon Sep 17 00:00:00 2001 From: Chuangbo Li Date: Mon, 12 Aug 2024 18:11:59 +0800 Subject: [PATCH 04/60] Fix command get policy works with relative policy path (#2051) --- hscontrol/grpcv1.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index a351048f..d4e10849 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -692,7 +692,8 @@ func (api headscaleV1APIServer) GetPolicy( }, nil case types.PolicyModeFile: // Read the file and return the contents as-is. - f, err := os.Open(api.h.cfg.Policy.Path) + absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path) + f, err := os.Open(absPath) if err != nil { return nil, err } From ac8491efec4b5ed088ce90e48d14136a1fe228da Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 19 Aug 2024 11:41:05 +0200 Subject: [PATCH 05/60] 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 --- .github/workflows/test-integration.yaml | 3 +- CHANGELOG.md | 7 +- cmd/headscale/headscale_test.go | 35 -- config-example.yaml | 81 ++-- flake.nix | 2 +- go.mod | 25 +- go.sum | 51 ++- hscontrol/mapper/tail.go | 3 +- hscontrol/mapper/tail_test.go | 4 +- hscontrol/noise.go | 2 +- hscontrol/types/config.go | 368 ++++++++++++------ hscontrol/types/config_test.go | 272 +++++++++++++ hscontrol/types/node.go | 49 ++- hscontrol/types/node_test.go | 8 +- .../testdata/base-domain-in-server-url.yaml | 16 + .../base-domain-not-in-server-url.yaml | 16 + hscontrol/types/testdata/dns_full.yaml | 37 ++ .../types/testdata/dns_full_no_magic.yaml | 37 ++ hscontrol/types/testdata/minimal.yaml | 3 + integration/dns_test.go | 246 ++++++++++++ integration/general_test.go | 68 ---- integration/hsic/config.go | 105 +---- integration/scenario.go | 10 +- integration/tailscale.go | 1 + integration/tsic/tsic.go | 40 ++ 25 files changed, 1036 insertions(+), 453 deletions(-) create mode 100644 hscontrol/types/config_test.go create mode 100644 hscontrol/types/testdata/base-domain-in-server-url.yaml create mode 100644 hscontrol/types/testdata/base-domain-not-in-server-url.yaml create mode 100644 hscontrol/types/testdata/dns_full.yaml create mode 100644 hscontrol/types/testdata/dns_full_no_magic.yaml create mode 100644 hscontrol/types/testdata/minimal.yaml create mode 100644 integration/dns_test.go diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index bf55e2de..6203e51b 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -37,6 +37,8 @@ jobs: - TestNodeRenameCommand - TestNodeMoveCommand - TestPolicyCommand + - TestResolveMagicDNS + - TestValidateResolvConf - TestDERPServerScenario - TestPingAllByIP - TestPingAllByIPPublicDERP @@ -45,7 +47,6 @@ jobs: - TestEphemeral2006DeletedTooQuickly - TestPingAllByHostname - TestTaildrop - - TestResolveMagicDNS - TestExpireNode - TestNodeOnlineStatus - TestPingAllByIPManyUpDown diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8787ad..93898f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Adds additional configuration for PostgreSQL for setting max open, idle connection and idle connection lifetime. - API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553) - Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611) - - The latest supported client is 1.38 + - The latest supported client is 1.42 - Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564) - If no DERP is configured, the server will fail to start, this can be because it cannot load the DERPMap from file or url. - Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611) @@ -43,9 +43,12 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - MagicDNS domains no longer contain usernames []() - This is in preperation to fix Headscales implementation of tags which currently does not correctly remove the link between a tagged device and a user. As tagged devices will not have a user, this will require a change to the DNS generation, removing the username, see [#1369](https://github.com/juanfont/headscale/issues/1369) for more information. - `use_username_in_magic_dns` can be used to turn this behaviour on again, but note that this option _will be removed_ when tags are fixed. - - This option brings Headscales behaviour in line with Tailscale. + - dns.base_domain can no longer be the same as (or part of) server_url. + - This option brings Headscales behaviour in line with Tailscale. - YAML files are no longer supported for headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) - HuJSON is now the only supported format for policy. +- DNS configuration has been restructured [#2034](https://github.com/juanfont/headscale/pull/2034) + - Please review the new [config-example.yaml](./config-example.yaml) for the new structure. ### Changes diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index c27fa20a..580caf17 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -63,7 +63,6 @@ func (*Suite) TestConfigFileLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") - c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert( util.GetFileMode("unix_socket_permission"), check.Equals, @@ -106,7 +105,6 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetString("tls_letsencrypt_hostname"), check.Equals, "") c.Assert(viper.GetString("tls_letsencrypt_listen"), check.Equals, ":http") c.Assert(viper.GetString("tls_letsencrypt_challenge_type"), check.Equals, "HTTP-01") - c.Assert(viper.GetStringSlice("dns_config.nameservers")[0], check.Equals, "1.1.1.1") c.Assert( util.GetFileMode("unix_socket_permission"), check.Equals, @@ -116,39 +114,6 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false) } -func (*Suite) TestDNSConfigLoading(c *check.C) { - tmpDir, err := os.MkdirTemp("", "headscale") - if err != nil { - c.Fatal(err) - } - defer os.RemoveAll(tmpDir) - - path, err := os.Getwd() - if err != nil { - c.Fatal(err) - } - - // Symlink the example config file - err = os.Symlink( - filepath.Clean(path+"/../../config-example.yaml"), - filepath.Join(tmpDir, "config.yaml"), - ) - if err != nil { - c.Fatal(err) - } - - // Load example config, it should load without validation errors - err = types.LoadConfig(tmpDir, false) - c.Assert(err, check.IsNil) - - dnsConfig, baseDomain := types.GetDNSConfig() - - c.Assert(dnsConfig.Nameservers[0].String(), check.Equals, "1.1.1.1") - c.Assert(dnsConfig.Resolvers[0].Addr, check.Equals, "1.1.1.1") - c.Assert(dnsConfig.Proxied, check.Equals, true) - c.Assert(baseDomain, check.Equals, "example.com") -} - func writeConfig(c *check.C, tmpDir string, configYaml []byte) { // Populate a custom config file configFile := filepath.Join(tmpDir, "config.yaml") diff --git a/config-example.yaml b/config-example.yaml index 8f6f01c3..40e5c8e4 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -224,43 +224,60 @@ policy: # - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # -dns_config: - # Whether to prefer using Headscale provided DNS or use local. - override_local_dns: true +# Please not that for the DNS configuration to have any effect, +# clients must have the `--accept-ds=true` option enabled. This is the +# default for the Tailscale client. This option is enabled by default +# in the Tailscale client. +# +# Setting _any_ of the configuration and `--accept-dns=true` on the +# clients will integrate with the DNS manager on the client or +# overwrite /etc/resolv.conf. +# https://tailscale.com/kb/1235/resolv-conf +# +# If you want stop Headscale from managing the DNS configuration +# all the fields under `dns` should be set to empty values. +dns: + # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). + # Only works if there is at least a nameserver defined. + magic_dns: true + + # Defines the base domain to create the hostnames for MagicDNS. + # This domain _must_ be different from the server_url domain. + # `base_domain` must be a FQDN, without the trailing dot. + # The FQDN of the hosts will be + # `hostname.base_domain` (e.g., _myhost.example.com_). + base_domain: example.com # List of DNS servers to expose to clients. nameservers: - - 1.1.1.1 + global: + - 1.1.1.1 + - 1.0.0.1 + - 2606:4700:4700::1111 + - 2606:4700:4700::1001 - # NextDNS (see https://tailscale.com/kb/1218/nextdns/). - # "abc123" is example NextDNS ID, replace with yours. - # - # With metadata sharing: - # nameservers: - # - https://dns.nextdns.io/abc123 - # - # Without metadata sharing: - # nameservers: - # - 2a07:a8c0::ab:c123 - # - 2a07:a8c1::ab:c123 + # NextDNS (see https://tailscale.com/kb/1218/nextdns/). + # "abc123" is example NextDNS ID, replace with yours. + # - https://dns.nextdns.io/abc123 - # Split DNS (see https://tailscale.com/kb/1054/dns/), - # list of search domains and the DNS to query for each one. - # - # restricted_nameservers: - # foo.bar.com: - # - 1.1.1.1 - # darp.headscale.net: - # - 1.1.1.1 - # - 8.8.8.8 + # Split DNS (see https://tailscale.com/kb/1054/dns/), + # a map of domains and which DNS server to use for each. + split: + {} + # foo.bar.com: + # - 1.1.1.1 + # darp.headscale.net: + # - 1.1.1.1 + # - 8.8.8.8 - # Search domains to inject. - domains: [] + # Set custom DNS search domains. With MagicDNS enabled, + # your tailnet base_domain is always the first search domain. + search_domains: [] # Extra DNS records # so far only A-records are supported (on the tailscale side) # See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations - # extra_records: + extra_records: [] # - name: "grafana.myvpn.example.com" # type: "A" # value: "100.64.0.3" @@ -268,10 +285,6 @@ dns_config: # # you can also put it in one line # - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" } - # Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/). - # Only works if there is at least a nameserver defined. - magic_dns: true - # DEPRECATED # Use the username as part of the DNS name for nodes, with this option enabled: # node1.username.example.com @@ -281,12 +294,6 @@ dns_config: # while in upstream Tailscale, the username is not included. use_username_in_magic_dns: false - # Defines the base domain to create the hostnames for MagicDNS. - # `base_domain` must be a FQDNs, without the trailing dot. - # The FQDN of the hosts will be - # `hostname.user.base_domain` (e.g., _myhost.myuser.example.com_). - base_domain: example.com - # Unix socket used for the CLI to connect without authentication # Note: for production you will want to set this to something like: unix_socket: /var/run/headscale/headscale.sock diff --git a/flake.nix b/flake.nix index ed4f24de..ab608439 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-EorT2AVwA3usly/LcNor6r5UIhLCdj3L4O4ilgTIC2o="; + vendorHash = "sha256-08N9ZdUM3Lw0ad89Vpy01e/qJQoMRPj8n4Jd7Aecgjw="; subPackages = ["cmd/headscale"]; diff --git a/go.mod b/go.mod index e96bcc8a..71cd8c44 100644 --- a/go.mod +++ b/go.mod @@ -31,15 +31,15 @@ require ( github.com/samber/lo v1.39.0 github.com/sasha-s/go-deadlock v0.3.1 github.com/spf13/cobra v1.8.0 - github.com/spf13/viper v1.18.2 + github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.23.0 - golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 - golang.org/x/net v0.25.0 + golang.org/x/crypto v0.25.0 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 + golang.org/x/net v0.27.0 golang.org/x/oauth2 v0.20.0 golang.org/x/sync v0.7.0 google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 @@ -101,6 +101,7 @@ require ( github.com/go-jose/go-jose/v4 v4.0.1 // indirect github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect @@ -117,7 +118,6 @@ require ( github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -137,7 +137,6 @@ require ( github.com/kr/text v0.2.0 // indirect github.com/lib/pq v1.10.7 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect - github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -166,8 +165,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/safchain/ethtool v0.3.0 // indirect - github.com/sagikazarmark/locafero v0.4.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect @@ -195,16 +193,15 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.21.0 // indirect + golang.org/x/tools v0.23.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect modernc.org/libc v1.50.6 // indirect diff --git a/go.sum b/go.sum index a534a8e4..6bc69456 100644 --- a/go.sum +++ b/go.sum @@ -180,6 +180,8 @@ github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.0.0 h1:dhn8MZ1gZ0mzeodTG3jt5Vj/o87xZKuNAprG2mQfMfc= +github.com/go-viper/mapstructure/v2 v2.0.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= @@ -240,11 +242,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= @@ -312,8 +311,6 @@ github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -419,10 +416,8 @@ github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= -github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= -github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= +github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= @@ -443,8 +438,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= -github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/spf13/viper v1.20.0-alpha.6 h1:f65Cr/+2qk4GfHC0xqT/isoupQppwN5+VLRztUGTDbY= +github.com/spf13/viper v1.20.0-alpha.6/go.mod h1:CGBZzv0c9fOUASm6rfus4wdeIjR/04NOLq1P4KRhX3k= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -538,11 +533,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= -golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= @@ -555,8 +550,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -569,8 +564,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= @@ -615,8 +610,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -624,8 +619,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -633,8 +628,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -648,8 +643,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= -golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -681,8 +676,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index 92fbed81..d21e4d8d 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -36,8 +36,7 @@ func tailNodes( return tNodes, nil } -// tailNode converts a Node into a Tailscale Node. includeRoutes is false for shared nodes -// as per the expected behaviour in the official SaaS. +// tailNode converts a Node into a Tailscale Node. func tailNode( node *types.Node, capVer tailcfg.CapabilityVersion, diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index 47af68fe..ac50d5a6 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -55,12 +55,14 @@ func TestTailNode(t *testing.T) { { name: "empty-node", node: &types.Node{ - Hostinfo: &tailcfg.Hostinfo{}, + GivenName: "empty", + Hostinfo: &tailcfg.Hostinfo{}, }, pol: &policy.ACLPolicy{}, dnsConfig: &tailcfg.DNSConfig{}, baseDomain: "", want: &tailcfg.Node{ + Name: "empty", StableID: "0", Addresses: []netip.Prefix{}, AllowedIPs: []netip.Prefix{}, diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 360c7045..554be65c 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -166,7 +166,7 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error { } const ( - MinimumCapVersion tailcfg.CapabilityVersion = 58 + MinimumCapVersion tailcfg.CapabilityVersion = 61 ) // NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 0c077870..e938768e 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -20,6 +20,7 @@ import ( "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" "tailscale.com/types/dnstype" + "tailscale.com/util/set" ) const ( @@ -88,6 +89,20 @@ type Config struct { Tuning Tuning } +type DNSConfig struct { + MagicDNS bool `mapstructure:"magic_dns"` + BaseDomain string `mapstructure:"base_domain"` + Nameservers Nameservers + SearchDomains []string `mapstructure:"search_domains"` + ExtraRecords []tailcfg.DNSRecord `mapstructure:"extra_records"` + UserNameInMagicDNS bool `mapstructure:"use_username_in_magic_dns"` +} + +type Nameservers struct { + Global []string + Split map[string][]string +} + type SqliteConfig struct { Path string WriteAheadLog bool @@ -201,7 +216,8 @@ func LoadConfig(path string, isFile bool) error { } } - viper.SetEnvPrefix("headscale") + envPrefix := "headscale" + viper.SetEnvPrefix(envPrefix) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.AutomaticEnv() @@ -213,9 +229,13 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("log.level", "info") viper.SetDefault("log.format", TextLogFormat) - viper.SetDefault("dns_config", nil) - viper.SetDefault("dns_config.override_local_dns", true) - viper.SetDefault("dns_config.use_username_in_magic_dns", false) + viper.SetDefault("dns.magic_dns", true) + viper.SetDefault("dns.base_domain", "") + viper.SetDefault("dns.nameservers.global", []string{}) + viper.SetDefault("dns.nameservers.split", map[string]string{}) + viper.SetDefault("dns.search_domains", []string{}) + viper.SetDefault("dns.extra_records", []tailcfg.DNSRecord{}) + viper.SetDefault("dns.use_username_in_magic_dns", false) viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.stun.enabled", true) @@ -259,17 +279,33 @@ func LoadConfig(path string, isFile bool) error { } if err := viper.ReadInConfig(); err != nil { - log.Warn().Err(err).Msg("Failed to read configuration from disk") - return fmt.Errorf("fatal error reading config file: %w", err) } + depr := deprecator{ + warns: make(set.Set[string]), + fatals: make(set.Set[string]), + } + // Register aliases for backward compatibility // Has to be called _after_ viper.ReadInConfig() // https://github.com/spf13/viper/issues/560 // Alias the old ACL Policy path with the new configuration option. - registerAliasAndDeprecate("policy.path", "acl_policy_path") + depr.warnWithAlias("policy.path", "acl_policy_path") + + // Move dns_config -> dns + depr.warn("dns_config.override_local_dns") + depr.fatalIfNewKeyIsNotUsed("dns.magic_dns", "dns_config.magic_dns") + depr.fatalIfNewKeyIsNotUsed("dns.base_domain", "dns_config.base_domain") + depr.fatalIfNewKeyIsNotUsed("dns.nameservers.global", "dns_config.nameservers") + depr.fatalIfNewKeyIsNotUsed("dns.nameservers.split", "dns_config.restricted_nameservers") + depr.fatalIfNewKeyIsNotUsed("dns.search_domains", "dns_config.domains") + depr.fatalIfNewKeyIsNotUsed("dns.extra_records", "dns_config.extra_records") + depr.warn("dns_config.use_username_in_magic_dns") + depr.warn("dns.use_username_in_magic_dns") + + depr.Log() // Collect any validation errors and return them all at once var errorText string @@ -485,123 +521,131 @@ func GetDatabaseConfig() DatabaseConfig { } } -func GetDNSConfig() (*tailcfg.DNSConfig, string) { - if viper.IsSet("dns_config") { - dnsConfig := &tailcfg.DNSConfig{} +func DNS() (DNSConfig, error) { + var dns DNSConfig - overrideLocalDNS := viper.GetBool("dns_config.override_local_dns") + // TODO: Use this instead of manually getting settings when + // UnmarshalKey is compatible with Environment Variables. + // err := viper.UnmarshalKey("dns", &dns) + // if err != nil { + // return DNSConfig{}, fmt.Errorf("unmarshaling dns config: %w", err) + // } - if viper.IsSet("dns_config.nameservers") { - nameserversStr := viper.GetStringSlice("dns_config.nameservers") + dns.MagicDNS = viper.GetBool("dns.magic_dns") + dns.BaseDomain = viper.GetString("dns.base_domain") + dns.Nameservers.Global = viper.GetStringSlice("dns.nameservers.global") + dns.Nameservers.Split = viper.GetStringMapStringSlice("dns.nameservers.split") + dns.SearchDomains = viper.GetStringSlice("dns.search_domains") - nameservers := []netip.Addr{} - resolvers := []*dnstype.Resolver{} + if viper.IsSet("dns.extra_records") { + var extraRecords []tailcfg.DNSRecord - for _, nameserverStr := range nameserversStr { - // Search for explicit DNS-over-HTTPS resolvers - if strings.HasPrefix(nameserverStr, "https://") { - resolvers = append(resolvers, &dnstype.Resolver{ - Addr: nameserverStr, - }) - - // This nameserver can not be parsed as an IP address - continue - } - - // Parse nameserver as a regular IP - nameserver, err := netip.ParseAddr(nameserverStr) - if err != nil { - log.Error(). - Str("func", "getDNSConfig"). - Err(err). - Msgf("Could not parse nameserver IP: %s", nameserverStr) - } - - nameservers = append(nameservers, nameserver) - resolvers = append(resolvers, &dnstype.Resolver{ - Addr: nameserver.String(), - }) - } - - dnsConfig.Nameservers = nameservers - - if overrideLocalDNS { - dnsConfig.Resolvers = resolvers - } else { - dnsConfig.FallbackResolvers = resolvers - } + err := viper.UnmarshalKey("dns.extra_records", &extraRecords) + if err != nil { + return DNSConfig{}, fmt.Errorf("unmarshaling dns extra records: %w", err) } - if viper.IsSet("dns_config.restricted_nameservers") { - dnsConfig.Routes = make(map[string][]*dnstype.Resolver) - domains := []string{} - restrictedDNS := viper.GetStringMapStringSlice( - "dns_config.restricted_nameservers", - ) - for domain, restrictedNameservers := range restrictedDNS { - restrictedResolvers := make( - []*dnstype.Resolver, - len(restrictedNameservers), - ) - for index, nameserverStr := range restrictedNameservers { - nameserver, err := netip.ParseAddr(nameserverStr) - if err != nil { - log.Error(). - Str("func", "getDNSConfig"). - Err(err). - Msgf("Could not parse restricted nameserver IP: %s", nameserverStr) - } - restrictedResolvers[index] = &dnstype.Resolver{ - Addr: nameserver.String(), - } - } - dnsConfig.Routes[domain] = restrictedResolvers - domains = append(domains, domain) - } - dnsConfig.Domains = domains - } - - if viper.IsSet("dns_config.extra_records") { - var extraRecords []tailcfg.DNSRecord - - err := viper.UnmarshalKey("dns_config.extra_records", &extraRecords) - if err != nil { - log.Error(). - Str("func", "getDNSConfig"). - Err(err). - Msgf("Could not parse dns_config.extra_records") - } - - dnsConfig.ExtraRecords = extraRecords - } - - if viper.IsSet("dns_config.magic_dns") { - dnsConfig.Proxied = viper.GetBool("dns_config.magic_dns") - } - - var baseDomain string - if viper.IsSet("dns_config.base_domain") { - baseDomain = viper.GetString("dns_config.base_domain") - } else { - baseDomain = "headscale.net" // does not really matter when MagicDNS is not enabled - } - - if !viper.GetBool("dns_config.use_username_in_magic_dns") { - dnsConfig.Domains = []string{baseDomain} - } else { - log.Warn().Msg("DNS: Usernames in DNS has been deprecated, this option will be remove in future versions") - log.Warn().Msg("DNS: see 0.23.0 changelog for more information.") - } - - if domains := viper.GetStringSlice("dns_config.domains"); len(domains) > 0 { - dnsConfig.Domains = append(dnsConfig.Domains, domains...) - } - - log.Trace().Interface("dns_config", dnsConfig).Msg("DNS configuration loaded") - return dnsConfig, baseDomain + dns.ExtraRecords = extraRecords } - return nil, "" + dns.UserNameInMagicDNS = viper.GetBool("dns.use_username_in_magic_dns") + + return dns, nil +} + +// GlobalResolvers returns the global DNS resolvers +// defined in the config file. +// If a nameserver is a valid IP, it will be used as a regular resolver. +// If a nameserver is a valid URL, it will be used as a DoH resolver. +// If a nameserver is neither a valid URL nor a valid IP, it will be ignored. +func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { + var resolvers []*dnstype.Resolver + + for _, nsStr := range d.Nameservers.Global { + warn := "" + if _, err := netip.ParseAddr(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + + continue + } else { + warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if _, err := url.Parse(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + } else { + warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if warn != "" { + log.Warn().Msg(warn) + } + } + + return resolvers +} + +// SplitResolvers returns a map of domain to DNS resolvers. +// If a nameserver is a valid IP, it will be used as a regular resolver. +// If a nameserver is a valid URL, it will be used as a DoH resolver. +// If a nameserver is neither a valid URL nor a valid IP, it will be ignored. +func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { + routes := make(map[string][]*dnstype.Resolver) + for domain, nameservers := range d.Nameservers.Split { + var resolvers []*dnstype.Resolver + for _, nsStr := range nameservers { + warn := "" + if _, err := netip.ParseAddr(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + + continue + } else { + warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if _, err := url.Parse(nsStr); err == nil { + resolvers = append(resolvers, &dnstype.Resolver{ + Addr: nsStr, + }) + } else { + warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) + } + + if warn != "" { + log.Warn().Msg(warn) + } + } + routes[domain] = resolvers + } + + return routes +} + +func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { + cfg := tailcfg.DNSConfig{} + + if dns.BaseDomain == "" && dns.MagicDNS { + log.Fatal().Msg("dns.base_domain must be set when using MagicDNS (dns.magic_dns)") + } + + cfg.Proxied = dns.MagicDNS + cfg.ExtraRecords = dns.ExtraRecords + cfg.Resolvers = dns.GlobalResolvers() + + routes := dns.SplitResolvers() + cfg.Routes = routes + if dns.BaseDomain != "" { + cfg.Domains = []string{dns.BaseDomain} + } + cfg.Domains = append(cfg.Domains, dns.SearchDomains...) + + return &cfg } func PrefixV4() (*netip.Prefix, error) { @@ -693,7 +737,11 @@ func GetHeadscaleConfig() (*Config, error) { return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) } - dnsConfig, baseDomain := GetDNSConfig() + dnsConfig, err := DNS() + if err != nil { + return nil, err + } + derpConfig := GetDERPConfig() logTailConfig := GetLogTailConfig() randomizeClientPort := viper.GetBool("randomize_client_port") @@ -711,8 +759,23 @@ func GetHeadscaleConfig() (*Config, error) { oidcClientSecret = strings.TrimSpace(string(secretBytes)) } + serverURL := viper.GetString("server_url") + + // BaseDomain cannot be the same as the server URL. + // This is because Tailscale takes over the domain in BaseDomain, + // causing the headscale server and DERP to be unreachable. + // For Tailscale upstream, the following is true: + // - DERP run on their own domains + // - Control plane runs on login.tailscale.com/controlplane.tailscale.com + // - MagicDNS (BaseDomain) for users is on a *.ts.net domain per tailnet (e.g. tail-scale.ts.net) + // + // TODO(kradalby): remove dnsConfig.UserNameInMagicDNS check when removed. + if !dnsConfig.UserNameInMagicDNS && dnsConfig.BaseDomain != "" && strings.Contains(serverURL, dnsConfig.BaseDomain) { + return nil, errors.New("server_url cannot contain the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.") + } + return &Config{ - ServerURL: viper.GetString("server_url"), + ServerURL: serverURL, Addr: viper.GetString("listen_addr"), MetricsAddr: viper.GetString("metrics_listen_addr"), GRPCAddr: viper.GetString("grpc_listen_addr"), @@ -726,7 +789,7 @@ func GetHeadscaleConfig() (*Config, error) { NoisePrivateKeyPath: util.AbsolutePathFromConfigPath( viper.GetString("noise.private_key_path"), ), - BaseDomain: baseDomain, + BaseDomain: dnsConfig.BaseDomain, DERP: derpConfig, @@ -738,8 +801,8 @@ func GetHeadscaleConfig() (*Config, error) { TLS: GetTLSConfig(), - DNSConfig: dnsConfig, - DNSUserNameInMagicDNS: viper.GetBool("dns_config.use_username_in_magic_dns"), + DNSConfig: DNSToTailcfgDNS(dnsConfig), + DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS, ACMEEmail: viper.GetString("acme_email"), ACMEURL: viper.GetString("acme_url"), @@ -805,19 +868,70 @@ func IsCLIConfigured() bool { return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" } -// registerAliasAndDeprecate will register an alias between the newKey and the oldKey, +type deprecator struct { + warns set.Set[string] + fatals set.Set[string] +} + +// warnWithAlias will register an alias between the newKey and the oldKey, // and log a deprecation warning if the oldKey is set. -func registerAliasAndDeprecate(newKey, oldKey string) { +func (d *deprecator) warnWithAlias(newKey, oldKey string) { // NOTE: RegisterAlias is called with NEW KEY -> OLD KEY viper.RegisterAlias(newKey, oldKey) if viper.IsSet(oldKey) { - log.Warn().Msgf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey) + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q will be removed in the future.", oldKey, newKey, oldKey)) } } -// deprecateAndFatal will log a fatal deprecation warning if the oldKey is set. -func deprecateAndFatal(newKey, oldKey string) { +// fatal deprecates and adds an entry to the fatal list of options if the oldKey is set. +func (d *deprecator) fatal(newKey, oldKey string) { if viper.IsSet(oldKey) { - log.Fatal().Msgf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey) + d.fatals.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } +} + +// fatalIfNewKeyIsNotUsed deprecates and adds an entry to the fatal list of options if the oldKey is set and the new key is _not_ set. +// If the new key is set, a warning is emitted instead. +func (d *deprecator) fatalIfNewKeyIsNotUsed(newKey, oldKey string) { + if viper.IsSet(oldKey) && !viper.IsSet(newKey) { + d.fatals.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } else if viper.IsSet(oldKey) { + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } +} + +// warn deprecates and adds an option to log a warning if the oldKey is set. +func (d *deprecator) warnNoAlias(newKey, oldKey string) { + if viper.IsSet(oldKey) { + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated. Please use %q instead. %q has been removed.", oldKey, newKey, oldKey)) + } +} + +// warn deprecates and adds an entry to the warn list of options if the oldKey is set. +func (d *deprecator) warn(oldKey string) { + if viper.IsSet(oldKey) { + d.warns.Add(fmt.Sprintf("The %q configuration key is deprecated and has been removed. Please see the changelog for more details.", oldKey)) + } +} + +func (d *deprecator) String() string { + var b strings.Builder + + for _, w := range d.warns.Slice() { + fmt.Fprintf(&b, "WARN: %s\n", w) + } + + for _, f := range d.fatals.Slice() { + fmt.Fprintf(&b, "FATAL: %s\n", f) + } + + return b.String() +} + +func (d *deprecator) Log() { + if len(d.fatals) > 0 { + log.Fatal().Msg("\n" + d.String()) + } else if len(d.warns) > 0 { + log.Warn().Msg("\n" + d.String()) } } diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go new file mode 100644 index 00000000..7cf562b1 --- /dev/null +++ b/hscontrol/types/config_test.go @@ -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) + } + }) + } +} diff --git a/hscontrol/types/node.go b/hscontrol/types/node.go index 24e36535..04ca9f8d 100644 --- a/hscontrol/types/node.go +++ b/hscontrol/types/node.go @@ -394,40 +394,39 @@ func (node *Node) Proto() *v1.Node { } func (node *Node) GetFQDN(cfg *Config, baseDomain string) (string, error) { - var hostname string - if cfg.DNSConfig != nil && cfg.DNSConfig.Proxied { // MagicDNS - if node.GivenName == "" { - return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) - } + if node.GivenName == "" { + return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeHasNoGivenName) + } + hostname := node.GivenName + + if baseDomain != "" { hostname = fmt.Sprintf( "%s.%s", node.GivenName, baseDomain, ) + } - if cfg.DNSUserNameInMagicDNS { - if node.User.Name == "" { - return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) - } - - hostname = fmt.Sprintf( - "%s.%s.%s", - node.GivenName, - node.User.Name, - baseDomain, - ) + if cfg.DNSUserNameInMagicDNS { + if node.User.Name == "" { + return "", fmt.Errorf("failed to create valid FQDN: %w", ErrNodeUserHasNoName) } - if len(hostname) > MaxHostnameLength { - return "", fmt.Errorf( - "failed to create valid FQDN (%s): %w", - hostname, - ErrHostnameTooLong, - ) - } - } else { - hostname = node.GivenName + hostname = fmt.Sprintf( + "%s.%s.%s", + node.GivenName, + node.User.Name, + baseDomain, + ) + } + + if len(hostname) > MaxHostnameLength { + return "", fmt.Errorf( + "failed to create valid FQDN (%s): %w", + hostname, + ErrHostnameTooLong, + ) } return hostname, nil diff --git a/hscontrol/types/node_test.go b/hscontrol/types/node_test.go index 798a54d3..885edf5d 100644 --- a/hscontrol/types/node_test.go +++ b/hscontrol/types/node_test.go @@ -195,7 +195,7 @@ func TestNodeFQDN(t *testing.T) { DNSUserNameInMagicDNS: true, }, domain: "example.com", - want: "test", + want: "test.user.example.com", }, { name: "no-dnsconfig-with-username", @@ -206,7 +206,7 @@ func TestNodeFQDN(t *testing.T) { }, }, domain: "example.com", - want: "test", + want: "test.example.com", }, { name: "all-set", @@ -271,7 +271,7 @@ func TestNodeFQDN(t *testing.T) { DNSUserNameInMagicDNS: false, }, domain: "example.com", - want: "test", + want: "test.example.com", }, { name: "no-dnsconfig", @@ -282,7 +282,7 @@ func TestNodeFQDN(t *testing.T) { }, }, domain: "example.com", - want: "test", + want: "test.example.com", }, } diff --git a/hscontrol/types/testdata/base-domain-in-server-url.yaml b/hscontrol/types/testdata/base-domain-in-server-url.yaml new file mode 100644 index 00000000..683e0218 --- /dev/null +++ b/hscontrol/types/testdata/base-domain-in-server-url.yaml @@ -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 diff --git a/hscontrol/types/testdata/base-domain-not-in-server-url.yaml b/hscontrol/types/testdata/base-domain-not-in-server-url.yaml new file mode 100644 index 00000000..3af345e1 --- /dev/null +++ b/hscontrol/types/testdata/base-domain-not-in-server-url.yaml @@ -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 diff --git a/hscontrol/types/testdata/dns_full.yaml b/hscontrol/types/testdata/dns_full.yaml new file mode 100644 index 00000000..c47e7b0f --- /dev/null +++ b/hscontrol/types/testdata/dns_full.yaml @@ -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 diff --git a/hscontrol/types/testdata/dns_full_no_magic.yaml b/hscontrol/types/testdata/dns_full_no_magic.yaml new file mode 100644 index 00000000..ac3cc470 --- /dev/null +++ b/hscontrol/types/testdata/dns_full_no_magic.yaml @@ -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 diff --git a/hscontrol/types/testdata/minimal.yaml b/hscontrol/types/testdata/minimal.yaml new file mode 100644 index 00000000..1d9b1e00 --- /dev/null +++ b/hscontrol/types/testdata/minimal.yaml @@ -0,0 +1,3 @@ +noise: + private_key_path: "private_key.pem" +server_url: "https://derp.no" diff --git a/integration/dns_test.go b/integration/dns_test.go new file mode 100644 index 00000000..60f05199 --- /dev/null +++ b/integration/dns_test.go @@ -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)) + } + }) + } + +} diff --git a/integration/general_test.go b/integration/general_test.go index c17b977e..2819edb2 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -623,74 +623,6 @@ func TestTaildrop(t *testing.T) { } } -func TestResolveMagicDNS(t *testing.T) { - IntegrationSkip(t) - t.Parallel() - - scenario, err := NewScenario(dockertestMaxWait()) - assertNoErr(t, err) - defer scenario.Shutdown() - - spec := map[string]int{ - "magicdns1": len(MustTestVersions), - "magicdns2": len(MustTestVersions), - } - - err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{}, hsic.WithTestName("magicdns")) - assertNoErrHeadscaleEnv(t, err) - - allClients, err := scenario.ListTailscaleClients() - assertNoErrListClients(t, err) - - err = scenario.WaitForTailscaleSync() - assertNoErrSync(t, err) - - // assertClientsState(t, allClients) - - // Poor mans cache - _, err = scenario.ListTailscaleClientsFQDNs() - assertNoErrListFQDN(t, err) - - _, err = scenario.ListTailscaleClientsIPs() - assertNoErrListClientIPs(t, err) - - for _, client := range allClients { - for _, peer := range allClients { - // It is safe to ignore this error as we handled it when caching it - peerFQDN, _ := peer.FQDN() - - command := []string{ - "tailscale", - "ip", peerFQDN, - } - result, _, err := client.Execute(command) - if err != nil { - t.Fatalf( - "failed to execute resolve/ip command %s from %s: %s", - peerFQDN, - client.Hostname(), - err, - ) - } - - ips, err := peer.IPs() - if err != nil { - t.Fatalf( - "failed to get ips for %s: %s", - peer.Hostname(), - err, - ) - } - - for _, ip := range ips { - if !strings.Contains(result, ip.String()) { - t.Fatalf("ip %s is not found in \n%s\n", ip.String(), result) - } - } - } - } -} - func TestExpireNode(t *testing.T) { IntegrationSkip(t) t.Parallel() diff --git a/integration/hsic/config.go b/integration/hsic/config.go index 7953799e..c4d8b283 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -2,104 +2,6 @@ package hsic import "github.com/juanfont/headscale/hscontrol/types" -// const ( -// defaultEphemeralNodeInactivityTimeout = time.Second * 30 -// defaultNodeUpdateCheckInterval = time.Second * 10 -// ) - -// TODO(kradalby): This approach doesnt work because we cannot -// serialise our config object to YAML or JSON. -// func DefaultConfig() headscale.Config { -// derpMap, _ := url.Parse("https://controlplane.tailscale.com/derpmap/default") -// -// config := headscale.Config{ -// Log: headscale.LogConfig{ -// Level: zerolog.TraceLevel, -// }, -// ACL: headscale.GetACLConfig(), -// DBtype: "sqlite3", -// EphemeralNodeInactivityTimeout: defaultEphemeralNodeInactivityTimeout, -// NodeUpdateCheckInterval: defaultNodeUpdateCheckInterval, -// IPPrefixes: []netip.Prefix{ -// netip.MustParsePrefix("fd7a:115c:a1e0::/48"), -// netip.MustParsePrefix("100.64.0.0/10"), -// }, -// DNSConfig: &tailcfg.DNSConfig{ -// Proxied: true, -// Nameservers: []netip.Addr{ -// netip.MustParseAddr("127.0.0.11"), -// netip.MustParseAddr("1.1.1.1"), -// }, -// Resolvers: []*dnstype.Resolver{ -// { -// Addr: "127.0.0.11", -// }, -// { -// Addr: "1.1.1.1", -// }, -// }, -// }, -// BaseDomain: "headscale.net", -// -// DBpath: "/tmp/integration_test_db.sqlite3", -// -// PrivateKeyPath: "/tmp/integration_private.key", -// NoisePrivateKeyPath: "/tmp/noise_integration_private.key", -// Addr: "0.0.0.0:8080", -// MetricsAddr: "127.0.0.1:9090", -// ServerURL: "http://headscale:8080", -// -// DERP: headscale.DERPConfig{ -// URLs: []url.URL{ -// *derpMap, -// }, -// AutoUpdate: false, -// UpdateFrequency: 1 * time.Minute, -// }, -// } -// -// return config -// } - -// TODO: Reuse the actual configuration object above. -// Deprecated: use env function instead as it is easier to -// override. -func DefaultConfigYAML() string { - yaml := ` -log: - level: trace -acl_policy_path: "" -database: - type: sqlite3 - sqlite.path: /tmp/integration_test_db.sqlite3 -ephemeral_node_inactivity_timeout: 30m -prefixes: - v6: fd7a:115c:a1e0::/48 - v4: 100.64.0.0/10 -dns_config: - base_domain: headscale.net - magic_dns: true - domains: [] - nameservers: - - 127.0.0.11 - - 1.1.1.1 -private_key_path: /tmp/private.key -noise: - private_key_path: /tmp/noise_private.key -listen_addr: 0.0.0.0:8080 -metrics_listen_addr: 127.0.0.1:9090 -server_url: http://headscale:8080 - -derp: - urls: - - https://controlplane.tailscale.com/derpmap/default - auto_update_enabled: false - update_frequency: 1m -` - - return yaml -} - func MinimumConfigYAML() string { return ` private_key_path: /tmp/private.key @@ -117,10 +19,9 @@ func DefaultConfigEnv() map[string]string { "HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m", "HEADSCALE_PREFIXES_V4": "100.64.0.0/10", "HEADSCALE_PREFIXES_V6": "fd7a:115c:a1e0::/48", - "HEADSCALE_DNS_CONFIG_BASE_DOMAIN": "headscale.net", - "HEADSCALE_DNS_CONFIG_MAGIC_DNS": "true", - "HEADSCALE_DNS_CONFIG_DOMAINS": "", - "HEADSCALE_DNS_CONFIG_NAMESERVERS": "127.0.0.11 1.1.1.1", + "HEADSCALE_DNS_BASE_DOMAIN": "headscale.net", + "HEADSCALE_DNS_MAGIC_DNS": "true", + "HEADSCALE_DNS_NAMESERVERS_GLOBAL": "127.0.0.11 1.1.1.1", "HEADSCALE_PRIVATE_KEY_PATH": "/tmp/private.key", "HEADSCALE_NOISE_PRIVATE_KEY_PATH": "/tmp/noise_private.key", "HEADSCALE_LISTEN_ADDR": "0.0.0.0:8080", diff --git a/integration/scenario.go b/integration/scenario.go index bd004247..6476fd58 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -51,6 +51,8 @@ var ( tailscaleVersions2021 = map[string]bool{ "head": true, "unstable": true, + "1.70": true, // CapVer: not checked + "1.68": true, // CapVer: not checked "1.66": true, // CapVer: not checked "1.64": true, // CapVer: not checked "1.62": true, // CapVer: not checked @@ -62,10 +64,10 @@ var ( "1.50": true, // CapVer: 74 "1.48": true, // CapVer: 68 "1.46": true, // CapVer: 65 - "1.44": true, // CapVer: 63 - "1.42": true, // CapVer: 61 - "1.40": true, // CapVer: 61 - "1.38": true, // Oldest supported version, CapVer: 58 + "1.44": false, // CapVer: 63 + "1.42": false, // Oldest supported version, CapVer: 61 + "1.40": false, // CapVer: 61 + "1.38": false, // CapVer: 58 "1.36": false, // CapVer: 56 "1.34": false, // CapVer: 51 "1.32": false, // CapVer: 46 diff --git a/integration/tailscale.go b/integration/tailscale.go index 2ea3faa9..5b1baf1b 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -36,6 +36,7 @@ type TailscaleClient interface { Ping(hostnameOrIP string, opts ...tsic.PingOption) error Curl(url string, opts ...tsic.CurlOption) (string, error) ID() string + ReadFile(path string) ([]byte, error) // FailingPeersAsString returns a formatted-ish multi-line-string of peers in the client // and a bool indicating if the clients online count and peer count is equal. diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index 0e3c91f8..e1045ec3 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -1,6 +1,8 @@ package tsic import ( + "archive/tar" + "bytes" "context" "encoding/json" "errors" @@ -998,3 +1000,41 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { func (t *TailscaleInContainer) SaveLog(path string) error { return dockertestutil.SaveLog(t.pool, t.container, path) } + +// ReadFile reads a file from the Tailscale container. +// It returns the content of the file as a byte slice. +func (t *TailscaleInContainer) ReadFile(path string) ([]byte, error) { + tarBytes, err := integrationutil.FetchPathFromContainer(t.pool, t.container, path) + if err != nil { + return nil, fmt.Errorf("reading file from container: %w", err) + } + + var out bytes.Buffer + tr := tar.NewReader(bytes.NewReader(tarBytes)) + for { + hdr, err := tr.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + return nil, fmt.Errorf("reading tar header: %w", err) + } + + if !strings.Contains(path, hdr.Name) { + return nil, fmt.Errorf("file not found in tar archive, looking for: %s, header was: %s", path, hdr.Name) + } + + if _, err := io.Copy(&out, tr); err != nil { + return nil, fmt.Errorf("copying file to buffer: %w", err) + } + + // Only support reading the first tile + break + } + + if out.Len() == 0 { + return nil, fmt.Errorf("file is empty") + } + + return out.Bytes(), nil +} From fdc034e8ae7a3ac652c108ff8e83c43dc5464a27 Mon Sep 17 00:00:00 2001 From: nadongjun Date: Mon, 19 Aug 2024 18:47:52 +0900 Subject: [PATCH 06/60] Integrate GORM Logger with Zerolog and Add Configuration Options for Logging and Performance (#2040) * Integrate GORM logger with zerolog and add custom GORM configuration options * Add GormConfig struct to group GORM-related settings * Update debug mode instruction in config-example.yaml Co-authored-by: Kristoffer Dalby --------- Co-authored-by: Kristoffer Dalby --- config-example.yaml | 17 +++++++++ hscontrol/db/db.go | 5 +-- hscontrol/types/config.go | 23 ++++++++++++ hscontrol/util/log.go | 75 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) diff --git a/config-example.yaml b/config-example.yaml index 40e5c8e4..44e36b82 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -140,6 +140,23 @@ ephemeral_node_inactivity_timeout: 30m database: type: sqlite + # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". + debug: false + + # GORM configuration settings. + gorm: + # Enable prepared statements. + prepare_stmt: true + + # Enable parameterized queries. + parameterized_queries: true + + # Skip logging "record not found" errors. + skip_err_record_not_found: true + + # Threshold for slow queries in milliseconds. + slow_threshold: 1000 + # SQLite config sqlite: path: /var/lib/headscale/db.sqlite diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index c1908134..331dba54 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -426,7 +426,7 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { // TODO(kradalby): Integrate this with zerolog var dbLogger logger.Interface if cfg.Debug { - dbLogger = logger.Default + dbLogger = util.NewDBLogWrapper(&log.Logger, cfg.Gorm.SlowThreshold, cfg.Gorm.SkipErrRecordNotFound, cfg.Gorm.ParameterizedQueries) } else { dbLogger = logger.Default.LogMode(logger.Silent) } @@ -447,7 +447,8 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { db, err := gorm.Open( sqlite.Open(cfg.Sqlite.Path), &gorm.Config{ - Logger: dbLogger, + PrepareStmt: cfg.Gorm.PrepareStmt, + Logger: dbLogger, }, ) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index e938768e..bff80998 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -120,11 +120,22 @@ type PostgresConfig struct { ConnMaxIdleTimeSecs int } +type GormConfig struct { + Debug bool + SlowThreshold time.Duration + SkipErrRecordNotFound bool + ParameterizedQueries bool + PrepareStmt bool +} + type DatabaseConfig struct { // Type sets the database type, either "sqlite3" or "postgres" Type string Debug bool + // Type sets the gorm configuration + Gorm GormConfig + Sqlite SqliteConfig Postgres PostgresConfig } @@ -486,6 +497,11 @@ func GetDatabaseConfig() DatabaseConfig { type_ := viper.GetString("database.type") + skipErrRecordNotFound := viper.GetBool("database.gorm.skip_err_record_not_found") + slowThreshold := viper.GetDuration("database.gorm.slow_threshold") * time.Millisecond + parameterizedQueries := viper.GetBool("database.gorm.parameterized_queries") + prepareStmt := viper.GetBool("database.gorm.prepare_stmt") + switch type_ { case DatabaseSqlite, DatabasePostgres: break @@ -499,6 +515,13 @@ func GetDatabaseConfig() DatabaseConfig { return DatabaseConfig{ Type: type_, Debug: debug, + Gorm: GormConfig{ + Debug: debug, + SkipErrRecordNotFound: skipErrRecordNotFound, + SlowThreshold: slowThreshold, + ParameterizedQueries: parameterizedQueries, + PrepareStmt: prepareStmt, + }, Sqlite: SqliteConfig{ Path: util.AbsolutePathFromConfigPath( viper.GetString("database.sqlite.path"), diff --git a/hscontrol/util/log.go b/hscontrol/util/log.go index 41d667d1..12f646b1 100644 --- a/hscontrol/util/log.go +++ b/hscontrol/util/log.go @@ -1,7 +1,14 @@ package util import ( + "context" + "errors" + "time" + + "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "gorm.io/gorm" + gormLogger "gorm.io/gorm/logger" "tailscale.com/types/logger" ) @@ -14,3 +21,71 @@ func TSLogfWrapper() logger.Logf { log.Debug().Caller().Msgf(format, args...) } } + +type DBLogWrapper struct { + Logger *zerolog.Logger + Level zerolog.Level + Event *zerolog.Event + SlowThreshold time.Duration + SkipErrRecordNotFound bool + ParameterizedQueries bool +} + +func NewDBLogWrapper(origin *zerolog.Logger, slowThreshold time.Duration, skipErrRecordNotFound bool, parameterizedQueries bool) *DBLogWrapper { + l := &DBLogWrapper{ + Logger: origin, + Level: origin.GetLevel(), + SlowThreshold: slowThreshold, + SkipErrRecordNotFound: skipErrRecordNotFound, + ParameterizedQueries: parameterizedQueries, + } + + return l +} + +type DBLogWrapperOption func(*DBLogWrapper) + +func (l *DBLogWrapper) LogMode(gormLogger.LogLevel) gormLogger.Interface { + return l +} + +func (l *DBLogWrapper) Info(ctx context.Context, msg string, data ...interface{}) { + l.Logger.Info().Msgf(msg, data...) +} + +func (l *DBLogWrapper) Warn(ctx context.Context, msg string, data ...interface{}) { + l.Logger.Warn().Msgf(msg, data...) +} + +func (l *DBLogWrapper) Error(ctx context.Context, msg string, data ...interface{}) { + l.Logger.Error().Msgf(msg, data...) +} + +func (l *DBLogWrapper) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + elapsed := time.Since(begin) + sql, rowsAffected := fc() + fields := map[string]interface{}{ + "duration": elapsed, + "sql": sql, + "rowsAffected": rowsAffected, + } + + if err != nil && !(errors.Is(err, gorm.ErrRecordNotFound) && l.SkipErrRecordNotFound) { + l.Logger.Error().Err(err).Fields(fields).Msgf("") + return + } + + if l.SlowThreshold != 0 && elapsed > l.SlowThreshold { + l.Logger.Warn().Fields(fields).Msgf("") + return + } + + l.Logger.Debug().Fields(fields).Msgf("") +} + +func (l *DBLogWrapper) ParamsFilter(ctx context.Context, sql string, params ...interface{}) (string, []interface{}) { + if l.ParameterizedQueries { + return sql, nil + } + return sql, params +} From f99497340b1971f53b3aefec9c918e74523d0870 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 19 Aug 2024 12:06:55 +0200 Subject: [PATCH 07/60] add coderabbit config (#2060) Code Rabbit is one of these new fancy LLM code review tools. I am skeptical but we can try it for free and it might provide us with some value to let people get feedback while waiting for other people. Signed-off-by: Kristoffer Dalby --- .coderabbit.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..614f851b --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-GB" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + collapse_walkthrough: false + auto_review: + enabled: true + drafts: true +chat: + auto_reply: true From 84cb5d0aed3fe13329a3e28fc9eb1efc587a3b86 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 19 Aug 2024 13:03:01 +0200 Subject: [PATCH 08/60] make acl_policy_path fatal if policy.path is not set (#2041) --- config-example.yaml | 4 ++-- hscontrol/types/config.go | 2 +- hscontrol/types/config_test.go | 19 +++++++++++++++++++ .../types/testdata/policy-path-is-loaded.yaml | 18 ++++++++++++++++++ integration/hsic/config.go | 2 +- integration/hsic/hsic.go | 2 +- 6 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 hscontrol/types/testdata/policy-path-is-loaded.yaml diff --git a/config-example.yaml b/config-example.yaml index 44e36b82..2735eaf7 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -241,8 +241,8 @@ policy: # - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # -# Please not that for the DNS configuration to have any effect, -# clients must have the `--accept-ds=true` option enabled. This is the +# Please note that for the DNS configuration to have any effect, +# clients must have the `--accept-dns=true` option enabled. This is the # default for the Tailscale client. This option is enabled by default # in the Tailscale client. # diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index bff80998..30fa1c6b 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -303,7 +303,7 @@ func LoadConfig(path string, isFile bool) error { // https://github.com/spf13/viper/issues/560 // Alias the old ACL Policy path with the new configuration option. - depr.warnWithAlias("policy.path", "acl_policy_path") + depr.fatalIfNewKeyIsNotUsed("policy.path", "acl_policy_path") // Move dns_config -> dns depr.warn("dns_config.override_local_dns") diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go index 7cf562b1..2b36e45c 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -161,6 +161,25 @@ func TestReadConfig(t *testing.T) { }, wantErr: "", }, + { + name: "policy-path-is-loaded", + configPath: "testdata/policy-path-is-loaded.yaml", + setup: func(t *testing.T) (any, error) { + cfg, err := GetHeadscaleConfig() + if err != nil { + return nil, err + } + + return map[string]string{ + "policy.mode": string(cfg.Policy.Mode), + "policy.path": cfg.Policy.Path, + }, err + }, + want: map[string]string{ + "policy.mode": "file", + "policy.path": "/etc/policy.hujson", + }, + }, } for _, tt := range tests { diff --git a/hscontrol/types/testdata/policy-path-is-loaded.yaml b/hscontrol/types/testdata/policy-path-is-loaded.yaml new file mode 100644 index 00000000..da0d29cd --- /dev/null +++ b/hscontrol/types/testdata/policy-path-is-loaded.yaml @@ -0,0 +1,18 @@ +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" + +acl_policy_path: "/etc/acl_policy.yaml" +policy: + type: file + path: "/etc/policy.hujson" + +dns.magic_dns: false diff --git a/integration/hsic/config.go b/integration/hsic/config.go index c4d8b283..244470f2 100644 --- a/integration/hsic/config.go +++ b/integration/hsic/config.go @@ -13,7 +13,7 @@ noise: func DefaultConfigEnv() map[string]string { return map[string]string{ "HEADSCALE_LOG_LEVEL": "trace", - "HEADSCALE_ACL_POLICY_PATH": "", + "HEADSCALE_POLICY_PATH": "", "HEADSCALE_DATABASE_TYPE": "sqlite", "HEADSCALE_DATABASE_SQLITE_PATH": "/tmp/integration_test_db.sqlite3", "HEADSCALE_EPHEMERAL_NODE_INACTIVITY_TIMEOUT": "30m", diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 3794e085..0b5a6be3 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -82,7 +82,7 @@ type Option = func(c *HeadscaleInContainer) func WithACLPolicy(acl *policy.ACLPolicy) Option { return func(hsic *HeadscaleInContainer) { // TODO(kradalby): Move somewhere appropriate - hsic.env["HEADSCALE_ACL_POLICY_PATH"] = aclPolicyPath + hsic.env["HEADSCALE_POLICY_PATH"] = aclPolicyPath hsic.aclPolicy = acl } From 9bed76d4817ec0d41242974185b06829964fca37 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 11:05:43 +0000 Subject: [PATCH 09/60] flake.lock: Update (#2059) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 627b7598..c69f2280 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1723221148, - "narHash": "sha256-7pjpeQlZUNQ4eeVntytU3jkw9dFK3k1Htgk2iuXjaD8=", + "lastModified": 1723856861, + "narHash": "sha256-OTDg91+Zzs2SpU3csK4xVdSQFoG8cK1lNUwKmTqERyE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "154bcb95ad51bc257c2ce4043a725de6ca700ef6", + "rev": "cd7b95ee3725af7113bacbce91dd6549cee58ca5", "type": "github" }, "original": { From a68854ac33f224e898a01fe4a5dd4c6a6174c757 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 23 Aug 2024 15:28:54 +0200 Subject: [PATCH 10/60] upgrade go (1.23) and tailscale (1.72.1) (#2077) --- Dockerfile.debug | 2 +- Dockerfile.tailscale-HEAD | 2 +- flake.lock | 6 ++-- flake.nix | 6 ++-- go.mod | 27 ++++++++--------- go.sum | 64 ++++++++++++++++++++------------------- integration/route_test.go | 13 ++++---- 7 files changed, 61 insertions(+), 59 deletions(-) diff --git a/Dockerfile.debug b/Dockerfile.debug index 4e63dca8..e5066060 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -2,7 +2,7 @@ # and are in no way endorsed by Headscale's maintainers as an # official nor supported release or distribution. -FROM docker.io/golang:1.22-bookworm +FROM docker.io/golang:1.23-bookworm ARG VERSION=dev ENV GOPATH /go WORKDIR /go/src/headscale diff --git a/Dockerfile.tailscale-HEAD b/Dockerfile.tailscale-HEAD index f78d687a..92b0cae5 100644 --- a/Dockerfile.tailscale-HEAD +++ b/Dockerfile.tailscale-HEAD @@ -4,7 +4,7 @@ # This Dockerfile is more or less lifted from tailscale/tailscale # to ensure a similar build process when testing the HEAD of tailscale. -FROM golang:1.22-alpine AS build-env +FROM golang:1.23-alpine AS build-env WORKDIR /go/src diff --git a/flake.lock b/flake.lock index c69f2280..82daf973 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1723856861, - "narHash": "sha256-OTDg91+Zzs2SpU3csK4xVdSQFoG8cK1lNUwKmTqERyE=", + "lastModified": 1724363052, + "narHash": "sha256-Nf/iQWamRVAwAPFccQMfm5Qcf+rLLnU1rWG3f9orDVE=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cd7b95ee3725af7113bacbce91dd6549cee58ca5", + "rev": "5de1564aed415bf9d0f281461babc2d101dd49ff", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index ab608439..dbf4f38f 100644 --- a/flake.nix +++ b/flake.nix @@ -21,7 +21,7 @@ overlay = _: prev: let pkgs = nixpkgs.legacyPackages.${prev.system}; in rec { - headscale = pkgs.buildGo122Module rec { + headscale = pkgs.buildGo123Module rec { pname = "headscale"; version = headscaleVersion; src = pkgs.lib.cleanSource self; @@ -31,7 +31,7 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-08N9ZdUM3Lw0ad89Vpy01e/qJQoMRPj8n4Jd7Aecgjw="; + vendorHash = "sha256-hmBRtMPqewg4oqu2bc9HtE3wdCdl5v9MoBOOCsjYlE8="; subPackages = ["cmd/headscale"]; @@ -63,7 +63,7 @@ overlays = [self.overlay]; inherit system; }; - buildDeps = with pkgs; [git go_1_22 gnumake]; + buildDeps = with pkgs; [git go_1_23 gnumake]; devDeps = with pkgs; buildDeps ++ [ diff --git a/go.mod b/go.mod index 71cd8c44..a0797844 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,6 @@ module github.com/juanfont/headscale -go 1.22.0 - -toolchain go1.22.2 +go 1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 @@ -23,14 +21,14 @@ require ( github.com/patrickmn/go-cache v2.1.0+incompatible github.com/philip-bui/grpc-zerolog v1.0.1 github.com/pkg/profile v1.7.0 - github.com/prometheus/client_golang v1.18.0 - github.com/prometheus/common v0.46.0 + github.com/prometheus/client_golang v1.19.1 + github.com/prometheus/common v0.48.0 github.com/pterm/pterm v0.12.79 github.com/puzpuzpuz/xsync/v3 v3.1.0 github.com/rs/zerolog v1.32.0 github.com/samber/lo v1.39.0 github.com/sasha-s/go-deadlock v0.3.1 - github.com/spf13/cobra v1.8.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a @@ -49,7 +47,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gorm.io/driver/postgres v1.5.7 gorm.io/gorm v1.25.10 - tailscale.com v1.66.3 + tailscale.com v1.72.1 ) require ( @@ -81,6 +79,7 @@ require ( github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/coder/websocket v1.8.12 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect @@ -88,14 +87,14 @@ require ( github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect github.com/docker/cli v26.1.3+incompatible // indirect - github.com/docker/docker v26.1.3+incompatible // indirect + github.com/docker/docker v26.1.4+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/fgprof v0.9.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.5.0 // indirect - github.com/gaissmai/bart v0.4.1 // indirect + github.com/fxamacker/cbor/v2 v2.6.0 // indirect + github.com/gaissmai/bart v0.11.1 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect github.com/go-jose/go-jose/v4 v4.0.1 // indirect @@ -159,6 +158,7 @@ require ( github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect @@ -174,14 +174,14 @@ require ( github.com/subosito/gotenv v1.6.0 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect - github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 // indirect + github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect github.com/tailscale/setec v0.0.0-20240314234648-9da8e7407257 // indirect github.com/tailscale/squibble v0.0.0-20240418235321-9ee0eeb78185 // indirect github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect - github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/uio v0.0.0-20240118234441-a3c409a6018e // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect @@ -203,10 +203,9 @@ require ( golang.zx2c4.com/wireguard/windows v0.5.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 // indirect + gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect modernc.org/libc v1.50.6 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect modernc.org/sqlite v1.29.9 // indirect - nhooyr.io/websocket v1.8.10 // indirect ) diff --git a/go.sum b/go.sum index 6bc69456..fb5b93c0 100644 --- a/go.sum +++ b/go.sum @@ -99,10 +99,12 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= @@ -113,13 +115,13 @@ github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8 github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/mds v0.14.5 h1:2amuO4yCbQkaAyDoLO5iCbwbTRQZz4EpRhOejQbf4+8= github.com/creachadair/mds v0.14.5/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= -github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= -github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -134,8 +136,8 @@ github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v26.1.3+incompatible h1:lLCzRbrVZrljpVNobJu1J2FHk8V0s4BawoZippkc+xo= -github.com/docker/docker v26.1.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= +github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -155,10 +157,10 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE= -github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= -github.com/gaissmai/bart v0.4.1 h1:G1t58voWkNmT47lBDawH5QhtTDsdqRIO+ftq5x4P9Ls= -github.com/gaissmai/bart v0.4.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= +github.com/fxamacker/cbor/v2 v2.6.0 h1:sU6J2usfADwWlYDAFhZBQ6TnLFBHxgesMrQfQgk1tWA= +github.com/fxamacker/cbor/v2 v2.6.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= @@ -382,13 +384,15 @@ github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Q github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= -github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= +github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= +github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= @@ -434,8 +438,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -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.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.20.0-alpha.6 h1:f65Cr/+2qk4GfHC0xqT/isoupQppwN5+VLRztUGTDbY= @@ -462,8 +466,8 @@ github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= @@ -482,10 +486,10 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:t github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754 h1:iazWjqVHE6CbNam7WXRhi33Qad5o7a8LVYgVoILpZdI= -github.com/tailscale/wireguard-go v0.0.0-20240429185444-03c5a0ccf754/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= -github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9 h1:81P7rjnikHKTJ75EkjppvbwUfKHDHYk6LJpO5PZy8pA= -github.com/tailscale/xnet v0.0.0-20240117122442-62b9a7c569f9/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= @@ -540,8 +544,8 @@ golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= -golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -691,8 +695,8 @@ gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3 h1:/8/t5pz/mgdRXhYOIeqqYhFAQLE4DDGegc0Y4ZjyFJM= -gvisor.dev/gvisor v0.0.0-20240306221502-ee1e1f6070e3/go.mod h1:NQHVAzMwvZ+Qe3ElSiHmq9RUm1MdNHpUZ52fiEqvn+0= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= @@ -725,9 +729,7 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= -nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.66.3 h1:jpWat+hiobTtCosSV/c8D6S/ubgROf/S59MaIBdM9pY= -tailscale.com v1.66.3/go.mod h1:99BIV4U3UPw36Sva04xK2ZsEpVRUkY9jCdEDSAhaNGM= +tailscale.com v1.72.1 h1:hk82jek36ph2S3Tfsh57NVWKEm/pZ9nfUonvlowpfaA= +tailscale.com v1.72.1/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= diff --git a/integration/route_test.go b/integration/route_test.go index 48b6c07f..ed371642 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -17,6 +17,7 @@ import ( "github.com/juanfont/headscale/integration/tsic" "github.com/stretchr/testify/assert" "tailscale.com/types/ipproto" + "tailscale.com/types/views" "tailscale.com/wgengine/filter" ) @@ -1146,9 +1147,9 @@ func TestSubnetRouteACL(t *testing.T) { wantClientFilter := []filter.Match{ { - IPProto: []ipproto.Proto{ + IPProto: views.SliceOf([]ipproto.Proto{ ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, - }, + }), Srcs: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("100.64.0.2/32"), @@ -1178,9 +1179,9 @@ func TestSubnetRouteACL(t *testing.T) { wantSubnetFilter := []filter.Match{ { - IPProto: []ipproto.Proto{ + IPProto: views.SliceOf([]ipproto.Proto{ ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, - }, + }), Srcs: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("100.64.0.2/32"), @@ -1200,9 +1201,9 @@ func TestSubnetRouteACL(t *testing.T) { Caps: []filter.CapMatch{}, }, { - IPProto: []ipproto.Proto{ + IPProto: views.SliceOf([]ipproto.Proto{ ipproto.TCP, ipproto.UDP, ipproto.ICMPv4, ipproto.ICMPv6, - }, + }), Srcs: []netip.Prefix{ netip.MustParsePrefix("100.64.0.1/32"), netip.MustParsePrefix("100.64.0.2/32"), From 9c4c286696d7eaea3dc613c0112ca237d78232b3 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 23 Aug 2024 17:17:37 +0200 Subject: [PATCH 11/60] fix warning errs from beta2 (#2075) * remove default false for use usernames causing warning Fixes #2065 Signed-off-by: Kristoffer Dalby * Ensure DoH warnings are only emitted if err Fixes #2064 Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- hscontrol/types/config.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 30fa1c6b..0b7d63b7 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -246,7 +246,6 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("dns.nameservers.split", map[string]string{}) viper.SetDefault("dns.search_domains", []string{}) viper.SetDefault("dns.extra_records", []tailcfg.DNSRecord{}) - viper.SetDefault("dns.use_username_in_magic_dns", false) viper.SetDefault("derp.server.enabled", false) viper.SetDefault("derp.server.stun.enabled", true) @@ -600,6 +599,8 @@ func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { resolvers = append(resolvers, &dnstype.Resolver{ Addr: nsStr, }) + + continue } else { warn = fmt.Sprintf("Invalid global nameserver %q. Parsing error: %s ignoring", nsStr, err) } @@ -636,6 +637,8 @@ func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { resolvers = append(resolvers, &dnstype.Resolver{ Addr: nsStr, }) + + continue } else { warn = fmt.Sprintf("Invalid split dns nameserver %q. Parsing error: %s ignoring", nsStr, err) } From 827e3e83aec0a5f2ced4530c91ad18fd2871a815 Mon Sep 17 00:00:00 2001 From: dragon2611 Date: Tue, 27 Aug 2024 10:03:51 +0100 Subject: [PATCH 12/60] Issue 2045, Feature Request (#2071) Requiring someone to write a design doc/contribute to the feature shouldn't be a requirement for raising a feature request as users may lack the skills required to do this. --- .github/ISSUE_TEMPLATE/feature_request.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index b95cd5e6..70f1a146 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -25,9 +25,9 @@ body: description: Are you willing to contribute to the implementation of this feature? options: - label: I can write the design doc for this feature - required: true + required: false - label: I can contribute this feature - required: true + required: false - type: textarea attributes: label: How can it be implemented? From cf6a606d74313b8b4dd4d5b07ee9b6ea61690624 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 27 Aug 2024 18:54:28 +0200 Subject: [PATCH 13/60] fix route table migration wiping routes 0.22 -> 0.23 (#2076) --- .github/workflows/test.yml | 2 +- hscontrol/db/db.go | 22 ++- hscontrol/db/db_test.go | 168 ++++++++++++++++++ hscontrol/db/node.go | 7 +- hscontrol/db/node_test.go | 14 +- ...3-to-0-23-0-routes-are-dropped-2063.sqlite | Bin 0 -> 98304 bytes ...0-23-0-routes-fail-foreign-key-2076.sqlite | Bin 0 -> 57344 bytes hscontrol/util/test.go | 6 +- integration/route_test.go | 4 +- 9 files changed, 204 insertions(+), 19 deletions(-) create mode 100644 hscontrol/db/db_test.go create mode 100644 hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite create mode 100644 hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b03fc434..f4659332 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,4 +34,4 @@ jobs: - name: Run tests if: steps.changed-files.outputs.files == 'true' - run: nix develop --check + run: nix develop --command -- gotestsum diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 331dba54..3aaa7eeb 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -51,8 +51,8 @@ func NewHeadscaleDatabase( dbConn, gormigrate.DefaultOptions, []*gormigrate.Migration{ - // New migrations should be added as transactions at the end of this list. - // The initial commit here is quite messy, completely out of order and + // New migrations must be added as transactions at the end of this list. + // The initial migration here is quite messy, completely out of order and // has no versioning and is the tech debt of not having versioned migrations // prior to this point. This first migration is all DB changes to bring a DB // up to 0.23.0. @@ -123,9 +123,21 @@ func NewHeadscaleDatabase( } } - err = tx.AutoMigrate(&types.Route{}) - if err != nil { - return err + // Only run automigrate Route table if it does not exist. It has only been + // changed ones, when machines where renamed to nodes, which is covered + // further up. This whole initial integration is a mess and if AutoMigrate + // is ran on a 0.22 to 0.23 update, it will wipe all the routes. + if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { + err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error + if err != nil { + return err + } + } + if !tx.Migrator().HasTable(&types.Route{}) { + err = tx.AutoMigrate(&types.Route{}) + if err != nil { + return err + } } err = tx.AutoMigrate(&types.Node{}) diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go new file mode 100644 index 00000000..b32d93ce --- /dev/null +++ b/hscontrol/db/db_test.go @@ -0,0 +1,168 @@ +package db + +import ( + "fmt" + "io" + "net/netip" + "os" + "path/filepath" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/juanfont/headscale/hscontrol/types" + "github.com/stretchr/testify/assert" + "gorm.io/gorm" +) + +func TestMigrations(t *testing.T) { + ipp := func(p string) types.IPPrefix { + return types.IPPrefix(netip.MustParsePrefix(p)) + } + r := func(id uint64, p string, a, e, i bool) types.Route { + return types.Route{ + NodeID: id, + Prefix: ipp(p), + Advertised: a, + Enabled: e, + IsPrimary: i, + } + } + tests := []struct { + dbPath string + wantFunc func(*testing.T, *HSDatabase) + wantErr string + }{ + { + dbPath: "testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) { + return GetRoutes(rx) + }) + assert.NoError(t, err) + + assert.Len(t, routes, 10) + want := types.Routes{ + r(1, "0.0.0.0/0", true, true, false), + r(1, "::/0", true, true, false), + r(1, "10.9.110.0/24", true, true, true), + r(26, "172.100.100.0/24", true, true, true), + r(26, "172.100.100.0/24", true, false, false), + r(31, "0.0.0.0/0", true, true, false), + r(31, "0.0.0.0/0", true, false, false), + r(31, "::/0", true, true, false), + r(31, "::/0", true, false, false), + r(32, "192.168.0.24/32", true, true, true), + } + if diff := cmp.Diff(want, routes, cmpopts.IgnoreFields(types.Route{}, "Model", "Node"), cmp.Comparer(func(x, y types.IPPrefix) bool { + return x == y + })); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + }, + }, + { + dbPath: "testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + routes, err := Read(h.DB, func(rx *gorm.DB) (types.Routes, error) { + return GetRoutes(rx) + }) + assert.NoError(t, err) + + assert.Len(t, routes, 4) + want := types.Routes{ + // These routes exists, but have no nodes associated with them + // when the migration starts. + // r(1, "0.0.0.0/0", true, true, false), + // r(1, "::/0", true, true, false), + // r(3, "0.0.0.0/0", true, true, false), + // r(3, "::/0", true, true, false), + // r(5, "0.0.0.0/0", true, true, false), + // r(5, "::/0", true, true, false), + // r(6, "0.0.0.0/0", true, true, false), + // r(6, "::/0", true, true, false), + // r(6, "10.0.0.0/8", true, false, false), + // r(7, "0.0.0.0/0", true, true, false), + // r(7, "::/0", true, true, false), + // r(7, "10.0.0.0/8", true, false, false), + // r(9, "0.0.0.0/0", true, true, false), + // r(9, "::/0", true, true, false), + // r(9, "10.0.0.0/8", true, true, false), + // r(11, "0.0.0.0/0", true, true, false), + // r(11, "::/0", true, true, false), + // r(11, "10.0.0.0/8", true, true, true), + // r(12, "0.0.0.0/0", true, true, false), + // r(12, "::/0", true, true, false), + // r(12, "10.0.0.0/8", true, false, false), + // + // These nodes exists, so routes should be kept. + r(13, "10.0.0.0/8", true, false, false), + r(13, "0.0.0.0/0", true, true, false), + r(13, "::/0", true, true, false), + r(13, "10.18.80.2/32", true, true, true), + } + if diff := cmp.Diff(want, routes, cmpopts.IgnoreFields(types.Route{}, "Model", "Node"), cmp.Comparer(func(x, y types.IPPrefix) bool { + return x == y + })); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.dbPath, func(t *testing.T) { + dbPath, err := testCopyOfDatabase(tt.dbPath) + if err != nil { + t.Fatalf("copying db for test: %s", err) + } + + hsdb, err := NewHeadscaleDatabase(types.DatabaseConfig{ + Type: "sqlite3", + Sqlite: types.SqliteConfig{ + Path: dbPath, + }, + }, "") + if err != nil && tt.wantErr != err.Error() { + t.Errorf("TestMigrations() unexpected error = %v, wantErr %v", err, tt.wantErr) + } + + if tt.wantFunc != nil { + tt.wantFunc(t, hsdb) + } + }) + } +} + +func testCopyOfDatabase(src string) (string, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return "", err + } + + if !sourceFileStat.Mode().IsRegular() { + return "", fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return "", err + } + defer source.Close() + + tmpDir, err := os.MkdirTemp("", "hsdb-test-*") + if err != nil { + return "", err + } + + fn := filepath.Base(src) + dst := filepath.Join(tmpDir, fn) + + destination, err := os.Create(dst) + if err != nil { + return "", err + } + defer destination.Close() + _, err = io.Copy(destination, source) + return dst, err +} diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index a2515ebf..a9e78a45 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -5,6 +5,7 @@ import ( "fmt" "net/netip" "sort" + "sync" "time" "github.com/juanfont/headscale/hscontrol/types" @@ -12,7 +13,6 @@ import ( "github.com/patrickmn/go-cache" "github.com/puzpuzpuz/xsync/v3" "github.com/rs/zerolog/log" - "github.com/sasha-s/go-deadlock" "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" @@ -724,7 +724,7 @@ func ExpireExpiredNodes(tx *gorm.DB, // It is used to delete ephemeral nodes that have disconnected and should be // cleaned up. type EphemeralGarbageCollector struct { - mu deadlock.Mutex + mu sync.Mutex deleteFunc func(types.NodeID) toBeDeleted map[types.NodeID]*time.Timer @@ -752,10 +752,9 @@ func (e *EphemeralGarbageCollector) Close() { // Schedule schedules a node for deletion after the expiry duration. func (e *EphemeralGarbageCollector) Schedule(nodeID types.NodeID, expiry time.Duration) { e.mu.Lock() - defer e.mu.Unlock() - timer := time.NewTimer(expiry) e.toBeDeleted[nodeID] = timer + e.mu.Unlock() go func() { select { diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index ad94f064..c83da120 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -609,12 +609,14 @@ func TestEphemeralGarbageCollectorOrder(t *testing.T) { }) go e.Start() - e.Schedule(1, 1*time.Second) - e.Schedule(2, 2*time.Second) - e.Schedule(3, 3*time.Second) - e.Schedule(4, 4*time.Second) - e.Cancel(2) - e.Cancel(4) + go e.Schedule(1, 1*time.Second) + go e.Schedule(2, 2*time.Second) + go e.Schedule(3, 3*time.Second) + go e.Schedule(4, 4*time.Second) + + time.Sleep(time.Second) + go e.Cancel(2) + go e.Cancel(4) time.Sleep(6 * time.Second) diff --git a/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite b/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-are-dropped-2063.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..10e1aaec5ed56ab30e47570788d37fa634fa0d82 GIT binary patch literal 98304 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 G_5TC3m>mEB literal 0 HcmV?d00001 diff --git a/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite b/hscontrol/db/testdata/0-22-3-to-0-23-0-routes-fail-foreign-key-2076.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..dbe969623060bbe12f2daa1cd00008788f80380f GIT binary patch literal 57344 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 Date: Wed, 28 Aug 2024 09:50:09 +0200 Subject: [PATCH 14/60] fix: correct a small spelling mistake (#2081) --- hscontrol/db/db.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 3aaa7eeb..99c3aa68 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -124,7 +124,7 @@ func NewHeadscaleDatabase( } // Only run automigrate Route table if it does not exist. It has only been - // changed ones, when machines where renamed to nodes, which is covered + // changed once, when machines where renamed to nodes, which is covered // further up. This whole initial integration is a mess and if AutoMigrate // is ran on a 0.22 to 0.23 update, it will wipe all the routes. if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { From 34361c6f827679284d306aafcd4795d17dc08799 Mon Sep 17 00:00:00 2001 From: Mike Poindexter Date: Thu, 29 Aug 2024 23:08:54 -0700 Subject: [PATCH 15/60] Fix FKs on sqlite migrations (#2083) --- hscontrol/db/db.go | 79 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index 99c3aa68..accf439e 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -123,21 +123,16 @@ func NewHeadscaleDatabase( } } - // Only run automigrate Route table if it does not exist. It has only been - // changed once, when machines where renamed to nodes, which is covered - // further up. This whole initial integration is a mess and if AutoMigrate - // is ran on a 0.22 to 0.23 update, it will wipe all the routes. + // Remove any invalid routes associated with a node that does not exist. if tx.Migrator().HasTable(&types.Route{}) && tx.Migrator().HasTable(&types.Node{}) { err := tx.Exec("delete from routes where node_id not in (select id from nodes)").Error if err != nil { return err } } - if !tx.Migrator().HasTable(&types.Route{}) { - err = tx.AutoMigrate(&types.Route{}) - if err != nil { - return err - } + err = tx.AutoMigrate(&types.Route{}) + if err != nil { + return err } err = tx.AutoMigrate(&types.Node{}) @@ -421,7 +416,7 @@ func NewHeadscaleDatabase( }, ) - if err = migrations.Migrate(); err != nil { + if err := runMigrations(cfg, dbConn, migrations); err != nil { log.Fatal().Err(err).Msgf("Migration failed: %v", err) } @@ -545,6 +540,70 @@ func openDB(cfg types.DatabaseConfig) (*gorm.DB, error) { ) } +func runMigrations(cfg types.DatabaseConfig, dbConn *gorm.DB, migrations *gormigrate.Gormigrate) error { + // Turn off foreign keys for the duration of the migration if using sqllite to + // prevent data loss due to the way the GORM migrator handles certain schema + // changes. + if cfg.Type == types.DatabaseSqlite { + var fkEnabled int + if err := dbConn.Raw("PRAGMA foreign_keys").Scan(&fkEnabled).Error; err != nil { + return fmt.Errorf("checking foreign key status: %w", err) + } + if fkEnabled == 1 { + if err := dbConn.Exec("PRAGMA foreign_keys = OFF").Error; err != nil { + return fmt.Errorf("disabling foreign keys: %w", err) + } + defer dbConn.Exec("PRAGMA foreign_keys = ON") + } + } + + if err := migrations.Migrate(); err != nil { + return err + } + + // Since we disabled foreign keys for the migration, we need to check for + // constraint violations manually at the end of the migration. + if cfg.Type == types.DatabaseSqlite { + type constraintViolation struct { + Table string + RowID int + Parent string + ConstraintIndex int + } + + var violatedConstraints []constraintViolation + + rows, err := dbConn.Raw("PRAGMA foreign_key_check").Rows() + if err != nil { + return err + } + + for rows.Next() { + var violation constraintViolation + if err := rows.Scan(&violation.Table, &violation.RowID, &violation.Parent, &violation.ConstraintIndex); err != nil { + return err + } + + violatedConstraints = append(violatedConstraints, violation) + } + _ = rows.Close() + + if len(violatedConstraints) > 0 { + for _, violation := range violatedConstraints { + log.Error(). + Str("table", violation.Table). + Int("row_id", violation.RowID). + Str("parent", violation.Parent). + Msg("Foreign key constraint violated") + } + + return fmt.Errorf("foreign key constraints violated") + } + } + + return nil +} + func (hsdb *HSDatabase) PingDB(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() From 76515d12d6bbeda26157c49fbffa390536951873 Mon Sep 17 00:00:00 2001 From: Mike Poindexter Date: Thu, 29 Aug 2024 23:20:29 -0700 Subject: [PATCH 16/60] Fix self notification on expiry update via oidc relogin (#2080) --- hscontrol/oidc.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hscontrol/oidc.go b/hscontrol/oidc.go index fe4d357c..72fefac3 100644 --- a/hscontrol/oidc.go +++ b/hscontrol/oidc.go @@ -526,7 +526,17 @@ func (h *Headscale) validateNodeForOIDCCallback( util.LogErr(err, "Failed to write response") } - ctx := types.NotifyCtx(context.Background(), "oidc-expiry", "na") + ctx := types.NotifyCtx(context.Background(), "oidc-expiry-self", node.Hostname) + h.nodeNotifier.NotifyByNodeID( + ctx, + types.StateUpdate{ + Type: types.StateSelfUpdate, + ChangeNodes: []types.NodeID{node.ID}, + }, + node.ID, + ) + + ctx = types.NotifyCtx(context.Background(), "oidc-expiry-peers", node.Hostname) h.nodeNotifier.NotifyWithIgnore(ctx, types.StateUpdateExpire(node.ID, expiry), node.ID) return nil, true, nil From fffd9d7ee9f17f1aeee1e21d72413b8ca92fa674 Mon Sep 17 00:00:00 2001 From: Roman Zabaluev Date: Fri, 30 Aug 2024 11:20:07 +0400 Subject: [PATCH 17/60] Update ACLs file format docs (#2066) --- docs/acls.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/acls.md b/docs/acls.md index 096dbea0..2330cda9 100644 --- a/docs/acls.md +++ b/docs/acls.md @@ -43,8 +43,7 @@ servers. Note: Users will be created automatically when users authenticate with the Headscale server. -ACLs could be written either on [huJSON](https://github.com/tailscale/hujson) -or YAML. Check the [test ACLs](../tests/acls) for further information. +ACLs have to be written in [huJSON](https://github.com/tailscale/hujson). Check the [test ACLs](../tests/acls) for further information. When registering the servers we will need to add the flag `--advertise-tags=tag:,tag:`, and the user that is From 2b5e52b08b1e36944c3b101b8a365e66c638b3e5 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 30 Aug 2024 16:58:29 +0200 Subject: [PATCH 18/60] validate policy against nodes, error if not valid (#2089) * validate policy against nodes, error if not valid this commit aims to improve the feedback of "runtime" policy errors which would only manifest when the rules are compiled to filter rules with nodes. this change will in; file-based mode load the nodes from the db and try to compile the rules on start up and return an error if they would not work as intended. database-based mode prevent a new ACL being written to the database if it does not compile with the current set of node. Fixes #2073 Fixes #2044 Signed-off-by: Kristoffer Dalby * ensure stderr can be used in err checks Signed-off-by: Kristoffer Dalby * test policy set validation Signed-off-by: Kristoffer Dalby * add new integration test to ghaction Signed-off-by: Kristoffer Dalby * add back defer for cli tst Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/app.go | 26 +++++++++ hscontrol/grpcv1.go | 29 +++++++++- integration/cli_test.go | 74 +++++++++++++++++++++++++ integration/dockertestutil/execute.go | 2 +- integration/hsic/hsic.go | 2 +- 6 files changed, 129 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index 6203e51b..aa220261 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -37,6 +37,7 @@ jobs: - TestNodeRenameCommand - TestNodeMoveCommand - TestPolicyCommand + - TestPolicyBrokenConfigCommand - TestResolveMagicDNS - TestValidateResolvConf - TestDERPServerScenario diff --git a/hscontrol/app.go b/hscontrol/app.go index b66e939b..087d2f2a 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -1001,6 +1001,32 @@ func (h *Headscale) loadACLPolicy() error { if err != nil { return fmt.Errorf("failed to load ACL policy from file: %w", err) } + + // Validate and reject configuration that would error when applied + // when creating a map response. This requires nodes, so there is still + // a scenario where they might be allowed if the server has no nodes + // yet, but it should help for the general case and for hot reloading + // configurations. + // Note that this check is only done for file-based policies in this function + // as the database-based policies are checked in the gRPC API where it is not + // allowed to be written to the database. + nodes, err := h.db.ListNodes() + if err != nil { + return fmt.Errorf("loading nodes from database to validate policy: %w", err) + } + + _, err = pol.CompileFilterRules(nodes) + if err != nil { + return fmt.Errorf("verifying policy rules: %w", err) + } + + if len(nodes) > 0 { + _, err = pol.CompileSSHPolicy(nodes[0], nodes) + if err != nil { + return fmt.Errorf("verifying SSH rules: %w", err) + } + } + case types.PolicyModeDB: p, err := h.db.GetPolicy() if err != nil { diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index d4e10849..83048bec 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -4,6 +4,7 @@ package hscontrol import ( "context" "errors" + "fmt" "io" "os" "sort" @@ -721,9 +722,31 @@ func (api headscaleV1APIServer) SetPolicy( p := request.GetPolicy() - valid, err := policy.LoadACLPolicyFromBytes([]byte(p)) + pol, err := policy.LoadACLPolicyFromBytes([]byte(p)) if err != nil { - return nil, err + return nil, fmt.Errorf("loading ACL policy file: %w", err) + } + + // Validate and reject configuration that would error when applied + // when creating a map response. This requires nodes, so there is still + // a scenario where they might be allowed if the server has no nodes + // yet, but it should help for the general case and for hot reloading + // configurations. + nodes, err := api.h.db.ListNodes() + if err != nil { + return nil, fmt.Errorf("loading nodes from database to validate policy: %w", err) + } + + _, err = pol.CompileFilterRules(nodes) + if err != nil { + return nil, fmt.Errorf("verifying policy rules: %w", err) + } + + if len(nodes) > 0 { + _, err = pol.CompileSSHPolicy(nodes[0], nodes) + if err != nil { + return nil, fmt.Errorf("verifying SSH rules: %w", err) + } } updated, err := api.h.db.SetPolicy(p) @@ -731,7 +754,7 @@ func (api headscaleV1APIServer) SetPolicy( return nil, err } - api.h.ACLPolicy = valid + api.h.ACLPolicy = pol ctx := types.NotifyCtx(context.Background(), "acl-update", "na") api.h.nodeNotifier.NotifyAll(ctx, types.StateUpdate{ diff --git a/integration/cli_test.go b/integration/cli_test.go index 088db786..9e7d179f 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -1676,3 +1676,77 @@ func TestPolicyCommand(t *testing.T) { assert.Len(t, output.ACLs, 1) assert.Equal(t, output.TagOwners["tag:exists"], []string{"policy-user"}) } + +func TestPolicyBrokenConfigCommand(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.Shutdown() + + spec := map[string]int{ + "policy-user": 1, + } + + err = scenario.CreateHeadscaleEnv( + spec, + []tsic.Option{}, + hsic.WithTestName("clins"), + hsic.WithConfigEnv(map[string]string{ + "HEADSCALE_POLICY_MODE": "database", + }), + ) + assertNoErr(t, err) + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + p := policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + // This is an unknown action, so it will return an error + // and the config will not be applied. + Action: "acccept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:exists": {"policy-user"}, + }, + } + + pBytes, _ := json.Marshal(p) + + policyFilePath := "/etc/headscale/policy.json" + + err = headscale.WriteFile(policyFilePath, pBytes) + assertNoErr(t, err) + + // No policy is present at this time. + // Add a new policy from a file. + _, err = headscale.Execute( + []string{ + "headscale", + "policy", + "set", + "-f", + policyFilePath, + }, + ) + assert.ErrorContains(t, err, "verifying policy rules: invalid action") + + // The new policy was invalid, the old one should still be in place, which + // is none. + _, err = headscale.Execute( + []string{ + "headscale", + "policy", + "get", + "--output", + "json", + }, + ) + assert.ErrorContains(t, err, "acl policy not found") +} diff --git a/integration/dockertestutil/execute.go b/integration/dockertestutil/execute.go index 5a8e92b3..1b41e324 100644 --- a/integration/dockertestutil/execute.go +++ b/integration/dockertestutil/execute.go @@ -62,7 +62,7 @@ func ExecuteCommand( exitCode, err := resource.Exec( cmd, dockertest.ExecOptions{ - Env: append(env, "HEADSCALE_LOG_LEVEL=disabled"), + Env: append(env, "HEADSCALE_LOG_LEVEL=info"), StdOut: &stdout, StdErr: &stderr, }, diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 0b5a6be3..bef05818 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -551,7 +551,7 @@ func (t *HeadscaleInContainer) Execute( log.Printf("command stdout: %s\n", stdout) } - return "", err + return stdout, fmt.Errorf("executing command in docker: %w, stderr: %s", err, stderr) } return stdout, nil From cb0e2e44764b7f925aa38b742d5eb42b97814aaf Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 30 Aug 2024 16:59:24 +0200 Subject: [PATCH 19/60] various doc updates in prep for 0.23 (#2091) * various doc updates in prep for 0.23 Signed-off-by: Kristoffer Dalby * add note discouraging postgresql Signed-off-by: Kristoffer Dalby * Update docs/faq.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * remove entry for glossary in doc Signed-off-by: Kristoffer Dalby * fix typo Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- config-example.yaml | 5 ++++ docs/faq.md | 2 +- docs/glossary.md | 6 ----- docs/index.md | 7 +----- docs/running-headscale-container.md | 36 +++++------------------------ mkdocs.yml | 1 - 6 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 docs/glossary.md diff --git a/config-example.yaml b/config-example.yaml index 2735eaf7..37c205e1 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -138,6 +138,9 @@ disable_check_updates: false ephemeral_node_inactivity_timeout: 30m database: + # Database type. Available options: sqlite, postgres + # Please not that using Postgres is highly discouraged as it is only supported for legacy reasons. + # All new development, testing and optimisations are done with SQLite in mind. type: sqlite # Enable debug mode. This setting requires the log.level to be set to "debug" or "trace". @@ -166,6 +169,8 @@ database: write_ahead_log: true # # Postgres config + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. + # See database.type for more information. # postgres: # # If using a Unix socket to connect to Postgres, set the socket path in the 'host' field and leave 'port' blank. # host: localhost diff --git a/docs/faq.md b/docs/faq.md index ba30911b..2a459967 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -31,7 +31,7 @@ We are more than happy to exchange emails, or to have dedicated calls before a P ## When/Why is Feature X going to be implemented? -We don't know. We might be working on it. If you want to help, please send us a PR. +We don't know. We might be working on it. If you're interested in contributing, please post a feature request about it. Please be aware that there are a number of reasons why we might not accept specific contributions: diff --git a/docs/glossary.md b/docs/glossary.md deleted file mode 100644 index f42941a6..00000000 --- a/docs/glossary.md +++ /dev/null @@ -1,6 +0,0 @@ -# Glossary - -| Term | Description | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| Machine | A machine is a single entity connected to `headscale`, typically an installation of Tailscale. Also known as **Node** | -| Namespace | A namespace was a logical grouping of machines "owned" by the same entity, in Tailscale, this is typically a User (This is now called user) | diff --git a/docs/index.md b/docs/index.md index f0b8bb00..f1b6e1b1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,12 +31,7 @@ buttons available in the repo. Headscale is "Open Source, acknowledged contribution", this means that any contribution will have to be discussed with the Maintainers before being submitted. -This model has been chosen to reduce the risk of burnout by limiting the -maintenance overhead of reviewing and validating third-party code. - -Headscale is open to code contributions for bug fixes without discussion. - -If you find mistakes in the documentation, please submit a fix to the documentation. +Please see [CONTRIBUTING.md](https://github.com/juanfont/headscale/blob/main/CONTRIBUTING.md) for more information. ## About diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 73c1107e..8f5cc7f9 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -42,36 +42,12 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca curl https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml -o ./config/config.yaml ``` - - **(Advanced)** If you would like to hand craft a config file **instead** of downloading the example config file, create a blank `headscale` configuration in the headscale directory to edit: + Modify the config file to your preferences before launching Docker container. + Here are some settings that you likely want: - ```shell - touch ./config/config.yaml - ``` - - Modify the config file to your preferences before launching Docker container. - Here are some settings that you likely want: - - ```yaml - # Change to your hostname or host IP - server_url: http://your-host-name:8080 - # Listen to 0.0.0.0 so it's accessible outside the container - metrics_listen_addr: 0.0.0.0:9090 - # The default /var/lib/headscale path is not writable in the container - noise: - private_key_path: /etc/headscale/noise_private.key - # The default /var/lib/headscale path is not writable in the container - derp: - private_key_path: /etc/headscale/private.key - # The default /var/run/headscale path is not writable in the container - unix_socket: /etc/headscale/headscale.sock - # The default /var/lib/headscale path is not writable in the container - database.type: sqlite3 - database.sqlite.path: /etc/headscale/db.sqlite - ``` - - Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding - `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` - in the next step. + Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding + `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` + in the next step. 1. Start the headscale server while working in the host headscale directory: @@ -95,7 +71,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca ```yaml version: "3.7" - + services: headscale: image: headscale/headscale:0.22.3 diff --git a/mkdocs.yml b/mkdocs.yml index 86a15469..2dca103d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -144,4 +144,3 @@ nav: - Proposals: - ACLs: proposals/001-acls.md - Better routing: proposals/002-better-routing.md - - Glossary: glossary.md From 1193a50e9ed260324de76e23a5744f6473ca0386 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Fri, 30 Aug 2024 16:59:37 +0200 Subject: [PATCH 20/60] oldest client supported, not latest (#2086) --- CHANGELOG.md | 2 +- README.md | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93898f38..fa5d7f74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Adds additional configuration for PostgreSQL for setting max open, idle connection and idle connection lifetime. - API: Machine is now Node [#1553](https://github.com/juanfont/headscale/pull/1553) - Remove support for older Tailscale clients [#1611](https://github.com/juanfont/headscale/pull/1611) - - The latest supported client is 1.42 + - The oldest supported client is 1.42 - Headscale checks that _at least_ one DERP is defined at start [#1564](https://github.com/juanfont/headscale/pull/1564) - If no DERP is configured, the server will fail to start, this can be because it cannot load the DERPMap from file or url. - Embedded DERP server requires a private key [#1611](https://github.com/juanfont/headscale/pull/1611) diff --git a/README.md b/README.md index 2ee8f4eb..03802e27 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ buttons available in the repo. - Taildrop (File Sharing) - [Access control lists](https://tailscale.com/kb/1018/acls/) - [MagicDNS](https://tailscale.com/kb/1081/magicdns) -- Support for multiple IP ranges in the tailnet - Dual stack (IPv4 and IPv6) - Routing advertising (including exit nodes) - Ephemeral nodes From a9a1a07e37ca32ba7d241eef6b96f07a53dfa114 Mon Sep 17 00:00:00 2001 From: nblock Date: Sun, 1 Sep 2024 15:08:06 +0200 Subject: [PATCH 21/60] Use dns: as config key (#2092) --- docs/dns-records.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dns-records.md b/docs/dns-records.md index d049c554..6c8fc42a 100644 --- a/docs/dns-records.md +++ b/docs/dns-records.md @@ -19,7 +19,7 @@ An example use case is to serve apps on the same host via a reverse proxy like N 1. Change the `config.yaml` to contain the desired records like so: ```yaml - dns_config: + dns: ... extra_records: - name: "prometheus.myvpn.example.com" From 976cbfa630599fb772549c3b305d5bda5eb3093c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Sep 2024 13:08:57 +0000 Subject: [PATCH 22/60] flake.lock: Update (#2078) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 82daf973..cd36fb42 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1724363052, - "narHash": "sha256-Nf/iQWamRVAwAPFccQMfm5Qcf+rLLnU1rWG3f9orDVE=", + "lastModified": 1725099143, + "narHash": "sha256-CHgumPZaC7z+WYx72WgaLt2XF0yUVzJS60rO4GZ7ytY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5de1564aed415bf9d0f281461babc2d101dd49ff", + "rev": "5629520edecb69630a3f4d17d3d33fc96c13f6fe", "type": "github" }, "original": { From ed71d230ebdf8cf1222b27969abf4c9077622502 Mon Sep 17 00:00:00 2001 From: nblock Date: Sun, 1 Sep 2024 15:09:47 +0200 Subject: [PATCH 23/60] Remove references to tests/acls from the documentation (#2088) --- docs/acls.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/acls.md b/docs/acls.md index 2330cda9..0b9a885f 100644 --- a/docs/acls.md +++ b/docs/acls.md @@ -3,7 +3,7 @@ Headscale implements the same policy ACLs as Tailscale.com, adapted to the self- For instance, instead of referring to users when defining groups you must use users (which are the equivalent to user/logins in Tailscale.com). -Please check https://tailscale.com/kb/1018/acls/, and `./tests/acls/` in this repo for working examples. +Please check https://tailscale.com/kb/1018/acls/ for further information. When using ACL's the User borders are no longer applied. All machines whichever the User have the ability to communicate with other hosts as @@ -43,7 +43,7 @@ servers. Note: Users will be created automatically when users authenticate with the Headscale server. -ACLs have to be written in [huJSON](https://github.com/tailscale/hujson). Check the [test ACLs](../tests/acls) for further information. +ACLs have to be written in [huJSON](https://github.com/tailscale/hujson). When registering the servers we will need to add the flag `--advertise-tags=tag:,tag:`, and the user that is From aa0f3d43cc179d14ceae904db035655d1525b126 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 2 Sep 2024 08:18:16 +0200 Subject: [PATCH 24/60] Fix typo in example config (#2095) --- config-example.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config-example.yaml b/config-example.yaml index 37c205e1..04a2f342 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -139,7 +139,7 @@ ephemeral_node_inactivity_timeout: 30m database: # Database type. Available options: sqlite, postgres - # Please not that using Postgres is highly discouraged as it is only supported for legacy reasons. + # Please note that using Postgres is highly discouraged as it is only supported for legacy reasons. # All new development, testing and optimisations are done with SQLite in mind. type: sqlite From 3101f895a7375266a76149b2c9d1a02f17295358 Mon Sep 17 00:00:00 2001 From: Mike Poindexter Date: Tue, 3 Sep 2024 00:22:17 -0700 Subject: [PATCH 25/60] Fix 764 (#2093) * Fix KeyExpiration when a zero time value has a timezone When a zero time value is loaded from JSON or a DB in a way that assigns it the local timezone, it does not roudtrip in JSON as a value for which IsZero returns true. This causes KeyExpiry to be treated as a far past value instead of a nilish value. See https://github.com/golang/go/issues/57040 * Fix whitespace * Ensure that postgresql is used for all tests when env var is set * Pass through value of HEADSCALE_INTEGRATION_POSTGRES env var * Add option to set timezone on headscale container * Add test for registration with auth key in alternate timezone --- .github/workflows/test-integration.yaml | 1 + CHANGELOG.md | 2 + hscontrol/mapper/tail.go | 4 +- hscontrol/mapper/tail_test.go | 66 +++++++++++++++++++++++++ integration/general_test.go | 10 +++- integration/hsic/hsic.go | 6 +++ integration/run.sh | 1 + integration/scenario.go | 8 +-- 8 files changed, 91 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index aa220261..d5b362b7 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -45,6 +45,7 @@ jobs: - TestPingAllByIPPublicDERP - TestAuthKeyLogoutAndRelogin - TestEphemeral + - TestEphemeralInAlternateTimezone - TestEphemeral2006DeletedTooQuickly - TestPingAllByHostname - TestTaildrop diff --git a/CHANGELOG.md b/CHANGELOG.md index fa5d7f74..bbb837fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Make registration page easier to use on mobile devices - Make write-ahead-log default on and configurable for SQLite [#1985](https://github.com/juanfont/headscale/pull/1985) - Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) +- Fix for registering nodes using preauthkeys when running on a postgres database in a non-UTC timezone. [#764](https://github.com/juanfont/headscale/issues/764) +- Make sure integration tests cover postgres for all scenarios ## 0.22.3 (2023-05-12) diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index d21e4d8d..b0878d1a 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -93,7 +93,7 @@ func tailNode( User: tailcfg.UserID(node.UserID), Key: node.NodeKey, - KeyExpiry: keyExpiry, + KeyExpiry: keyExpiry.UTC(), Machine: node.MachineKey, DiscoKey: node.DiscoKey, @@ -102,7 +102,7 @@ func tailNode( Endpoints: node.Endpoints, DERP: derp, Hostinfo: node.Hostinfo.View(), - Created: node.CreatedAt, + Created: node.CreatedAt.UTC(), Online: node.IsOnline, diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index ac50d5a6..f744c9c6 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -1,6 +1,7 @@ package mapper import ( + "encoding/json" "net/netip" "testing" "time" @@ -205,3 +206,68 @@ func TestTailNode(t *testing.T) { }) } } + +func TestNodeExpiry(t *testing.T) { + tp := func(t time.Time) *time.Time { + return &t + } + tests := []struct { + name string + exp *time.Time + wantTime time.Time + wantTimeZero bool + }{ + { + name: "no-expiry", + exp: nil, + wantTimeZero: true, + }, + { + name: "zero-expiry", + exp: &time.Time{}, + wantTimeZero: true, + }, + { + name: "localtime", + exp: tp(time.Time{}.Local()), + wantTimeZero: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := &types.Node{ + GivenName: "test", + Expiry: tt.exp, + } + tn, err := tailNode( + node, + 0, + &policy.ACLPolicy{}, + &types.Config{}, + ) + if err != nil { + t.Fatalf("nodeExpiry() error = %v", err) + } + + // Round trip the node through JSON to ensure the time is serialized correctly + seri, err := json.Marshal(tn) + if err != nil { + t.Fatalf("nodeExpiry() error = %v", err) + } + var deseri tailcfg.Node + err = json.Unmarshal(seri, &deseri) + if err != nil { + t.Fatalf("nodeExpiry() error = %v", err) + } + + if tt.wantTimeZero { + if !deseri.KeyExpiry.IsZero() { + t.Errorf("nodeExpiry() = %v, want zero", deseri.KeyExpiry) + } + } else if deseri.KeyExpiry != tt.wantTime { + t.Errorf("nodeExpiry() = %v, want %v", deseri.KeyExpiry, tt.wantTime) + } + }) + } +} diff --git a/integration/general_test.go b/integration/general_test.go index 2819edb2..6de00fd2 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -215,6 +215,14 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { } func TestEphemeral(t *testing.T) { + testEphemeralWithOptions(t, hsic.WithTestName("ephemeral")) +} + +func TestEphemeralInAlternateTimezone(t *testing.T) { + testEphemeralWithOptions(t, hsic.WithTestName("ephemeral-tz"), hsic.WithTimezone("America/Los_Angeles")) +} + +func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) { IntegrationSkip(t) t.Parallel() @@ -227,7 +235,7 @@ func TestEphemeral(t *testing.T) { "user2": len(MustTestVersions), } - headscale, err := scenario.Headscale(hsic.WithTestName("ephemeral")) + headscale, err := scenario.Headscale(opts...) assertNoErrHeadscaleEnv(t, err) for userName, clientCount := range spec { diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index bef05818..b9026225 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -211,6 +211,12 @@ func WithTuning(batchTimeout time.Duration, mapSessionChanSize int) Option { } } +func WithTimezone(timezone string) Option { + return func(hsic *HeadscaleInContainer) { + hsic.env["TZ"] = timezone + } +} + // New returns a new HeadscaleInContainer instance. func New( pool *dockertest.Pool, diff --git a/integration/run.sh b/integration/run.sh index 8cad3f02..137bcfb7 100755 --- a/integration/run.sh +++ b/integration/run.sh @@ -26,6 +26,7 @@ run_tests() { --volume "$PWD:$PWD" -w "$PWD"/integration \ --volume /var/run/docker.sock:/var/run/docker.sock \ --volume "$PWD"/control_logs:/tmp/control \ + -e "HEADSCALE_INTEGRATION_POSTGRES" \ golang:1 \ go test ./... \ -failfast \ diff --git a/integration/scenario.go b/integration/scenario.go index 6476fd58..075d1fd5 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -249,6 +249,10 @@ func (s *Scenario) Headscale(opts ...hsic.Option) (ControlServer, error) { return headscale, nil } + if usePostgresForTest { + opts = append(opts, hsic.WithPostgres()) + } + headscale, err := hsic.New(s.pool, s.network, opts...) if err != nil { return nil, fmt.Errorf("failed to create headscale container: %w", err) @@ -465,10 +469,6 @@ func (s *Scenario) CreateHeadscaleEnv( tsOpts []tsic.Option, opts ...hsic.Option, ) error { - if usePostgresForTest { - opts = append(opts, hsic.WithPostgres()) - } - headscale, err := s.Headscale(opts...) if err != nil { return err From d66c5e144f82a6198ee701264a585c959c4c985f Mon Sep 17 00:00:00 2001 From: nblock Date: Tue, 3 Sep 2024 13:04:20 +0200 Subject: [PATCH 26/60] Update documentation for 0.23 (#2096) * docs/acl: fix path to policy file * docs/exit-node: fixup for 0.23 * Add newlines between commands to improve readability * Use nodes instead on name * Remove query parameter from link to Tailscale docs * docs/remote-cli: fix formatting * Indent blocks below line numbers to restore numbering * Fix minor typos * docs/reverse-proxy: remove version information * Websocket support is always required now * s/see detail/see details * docs/exit-node: add warning to manual documentation * Replace the warning section with a warning admonition * Fix TODO link back to the regular linux documentation * docs/openbsd: fix typos * the database is created on-the-fly * docs/sealos: fix typos * docs/container: various fixes * Remove a stray sentence * Remove "headscale" before serve * Indent line continuation * Replace hardcoded 0.22 with * Fix path in debug image to /ko-app/headscale Fixes: #1822 aa --- docs/acls.md | 2 +- docs/exit-node.md | 24 ++++++++------ docs/remote-cli.md | 46 +++++++++++++------------- docs/reverse-proxy.md | 6 ++-- docs/running-headscale-container.md | 27 ++++++--------- docs/running-headscale-linux-manual.md | 12 ++----- docs/running-headscale-openbsd.md | 12 ++----- docs/running-headscale-sealos.md | 4 +-- 8 files changed, 58 insertions(+), 75 deletions(-) diff --git a/docs/acls.md b/docs/acls.md index 0b9a885f..4ab8fb46 100644 --- a/docs/acls.md +++ b/docs/acls.md @@ -52,7 +52,7 @@ a server they can register, the check of the tags is done on headscale server and only valid tags are applied. A tag is valid if the user that is registering it is allowed to do it. -To use ACLs in headscale, you must edit your config.yaml file. In there you will find a `acl_policy_path: ""` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/). +To use ACLs in headscale, you must edit your `config.yaml` file. In there you will find a `policy.path` parameter. This will need to point to your ACL file. More info on how these policies are written can be found [here](https://tailscale.com/kb/1018/acls/). Here are the ACL's to implement the same permissions as above: diff --git a/docs/exit-node.md b/docs/exit-node.md index 831652b3..797f42f4 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -21,21 +21,23 @@ To use a node as an exit node, IP forwarding must be enabled on the node. Check ```console $ # list nodes $ headscale routes list -ID | Machine | Prefix | Advertised | Enabled | Primary -1 | | 0.0.0.0/0 | false | false | - -2 | | ::/0 | false | false | - -3 | phobos | 0.0.0.0/0 | true | false | - -4 | phobos | ::/0 | true | false | - +ID | Node | Prefix | Advertised | Enabled | Primary +1 | | 0.0.0.0/0 | false | false | - +2 | | ::/0 | false | false | - +3 | phobos | 0.0.0.0/0 | true | false | - +4 | phobos | ::/0 | true | false | - + $ # enable routes for phobos $ headscale routes enable -r 3 $ headscale routes enable -r 4 + $ # Check node list again. The routes are now enabled. $ headscale routes list -ID | Machine | Prefix | Advertised | Enabled | Primary -1 | | 0.0.0.0/0 | false | false | - -2 | | ::/0 | false | false | - -3 | phobos | 0.0.0.0/0 | true | true | - -4 | phobos | ::/0 | true | true | - +ID | Node | Prefix | Advertised | Enabled | Primary +1 | | 0.0.0.0/0 | false | false | - +2 | | ::/0 | false | false | - +3 | phobos | 0.0.0.0/0 | true | true | - +4 | phobos | ::/0 | true | true | - ``` ## On the client @@ -46,4 +48,4 @@ The exit node can now be used with: $ sudo tailscale set --exit-node phobos ``` -Check the official [Tailscale documentation](https://tailscale.com/kb/1103/exit-nodes/?q=exit#step-3-use-the-exit-node) for how to do it on your device. +Check the official [Tailscale documentation](https://tailscale.com/kb/1103/exit-nodes#use-the-exit-node) for how to do it on your device. diff --git a/docs/remote-cli.md b/docs/remote-cli.md index 3d44eabc..14423852 100644 --- a/docs/remote-cli.md +++ b/docs/remote-cli.md @@ -47,40 +47,40 @@ headscale apikeys expire --prefix "" 3. Make `headscale` executable: -```shell -chmod +x /usr/local/bin/headscale -``` + ```shell + chmod +x /usr/local/bin/headscale + ``` -4. Configure the CLI through Environment Variables +4. Configure the CLI through environment variables -```shell -export HEADSCALE_CLI_ADDRESS=":" -export HEADSCALE_CLI_API_KEY="" -``` + ```shell + export HEADSCALE_CLI_ADDRESS=":" + export HEADSCALE_CLI_API_KEY="" + ``` -for example: + for example: -```shell -export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" -export HEADSCALE_CLI_API_KEY="abcde12345" -``` + ```shell + export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" + export HEADSCALE_CLI_API_KEY="abcde12345" + ``` -This will tell the `headscale` binary to connect to a remote instance, instead of looking -for a local instance (which is what it does on the server). + This will tell the `headscale` binary to connect to a remote instance, instead of looking + for a local instance (which is what it does on the server). -The API key is needed to make sure that your are allowed to access the server. The key is _not_ -needed when running directly on the server, as the connection is local. + The API key is needed to make sure that you are allowed to access the server. The key is _not_ + needed when running directly on the server, as the connection is local. 5. Test the connection -Let us run the headscale command to verify that we can connect by listing our nodes: + Let us run the headscale command to verify that we can connect by listing our nodes: -```shell -headscale nodes list -``` + ```shell + headscale nodes list + ``` -You should now be able to see a list of your nodes from your workstation, and you can -now control the `headscale` server from your workstation. + You should now be able to see a list of your nodes from your workstation, and you can + now control the `headscale` server from your workstation. ## Behind a proxy diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 23c61c26..b042b348 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -11,9 +11,9 @@ Running headscale behind a reverse proxy is useful when running multiple applica ### WebSockets -The reverse proxy MUST be configured to support WebSockets, as it is needed for clients running Tailscale v1.30+. +The reverse proxy MUST be configured to support WebSockets to communicate with Tailscale clients. -WebSockets support is required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml). +WebSockets support is also required when using the headscale embedded DERP server. In this case, you will also need to expose the UDP port used for STUN (by default, udp/3478). Please check our [config-example.yaml](https://github.com/juanfont/headscale/blob/main/config-example.yaml). ### Cloudflare @@ -80,7 +80,7 @@ Sending local reply with details upgrade_failed ### Envoy -You need add a new upgrade_type named `tailscale-control-protocol`. [see detail](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig) +You need to add a new upgrade_type named `tailscale-control-protocol`. [see details](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/network/http_connection_manager/v3/http_connection_manager.proto#extensions-filters-network-http-connection-manager-v3-httpconnectionmanager-upgradeconfig) ### Istio diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 8f5cc7f9..ef622f4e 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -22,12 +22,6 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca cd ./headscale ``` -1. Create an empty SQlite datebase in the headscale directory: - - ```shell - touch ./config/db.sqlite - ``` - 1. **(Strongly Recommended)** Download a copy of the [example configuration](https://github.com/juanfont/headscale/blob/main/config-example.yaml) from the headscale repository. - Using `wget`: @@ -43,7 +37,6 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca ``` Modify the config file to your preferences before launching Docker container. - Here are some settings that you likely want: Alternatively, you can mount `/var/lib` and `/var/run` from your host system by adding `--volume $(pwd)/lib:/var/lib/headscale` and `--volume $(pwd)/run:/var/run/headscale` @@ -59,7 +52,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca --publish 127.0.0.1:8080:8080 \ --publish 127.0.0.1:9090:9090 \ headscale/headscale: \ - headscale serve + serve ``` Note: use `0.0.0.0:8080:8080` instead of `127.0.0.1:8080:8080` if you want to expose the container externally. @@ -74,16 +67,16 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca services: headscale: - image: headscale/headscale:0.22.3 + image: headscale/headscale: restart: unless-stopped container_name: headscale ports: - "127.0.0.1:8080:8080" - "127.0.0.1:9090:9090" volumes: - # pls change [config_path] to the fullpath of the config folder just created - - [config_path]:/etc/headscale - command: headscale serve + # Please change to the fullpath of the config folder just created + - :/etc/headscale + command: serve ``` 1. Verify `headscale` is running: @@ -109,7 +102,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca ```shell docker exec headscale \ - headscale users create myfirstuser + headscale users create myfirstuser ``` ### Register a machine (normal login) @@ -124,7 +117,7 @@ To register a machine when running `headscale` in a container, take the headscal ```shell docker exec headscale \ - headscale --user myfirstuser nodes register --key + headscale --user myfirstuser nodes register --key ``` ### Register machine using a pre authenticated key @@ -152,7 +145,7 @@ To run the debug Docker container, use the exact same commands as above, but rep ### Executing commands in the debug container -The default command in the debug container is to run `headscale`, which is located at `/bin/headscale` inside the container. +The default command in the debug container is to run `headscale`, which is located at `/ko-app/headscale` inside the container. Additionally, the debug container includes a minimalist Busybox shell. @@ -162,10 +155,10 @@ To launch a shell in the container, use: docker run -it headscale/headscale:x.x.x-debug sh ``` -You can also execute commands directly, such as `ls /bin` in this example: +You can also execute commands directly, such as `ls /ko-app` in this example: ``` -docker run headscale/headscale:x.x.x-debug ls /bin +docker run headscale/headscale:x.x.x-debug ls /ko-app ``` Using `docker exec` allows you to run commands in an existing container. diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 3651c892..25d47638 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -1,9 +1,9 @@ # Running headscale on Linux -## Note: Outdated and "advanced" +!!! warning "Outdated and advanced" -This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not -want to use this documentation and rather look at the distro specific documentation (TODO LINK)[]. + This documentation is considered the "legacy"/advanced/manual version of the documentation, you most likely do not + want to use this documentation and rather look at the [distro specific documentation](./running-headscale-linux.md). ## Goal @@ -45,12 +45,6 @@ describing how to make `headscale` run properly in a server environment. headscale ``` -1. Create an empty SQLite database: - - ```shell - touch /var/lib/headscale/db.sqlite - ``` - 1. Create a `headscale` configuration: ```shell diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index 72c7bf79..f3e0548e 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -10,7 +10,7 @@ ## Goal This documentation has the goal of showing a user how-to install and run `headscale` on OpenBSD. -In additional to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd) +In addition to the "get up and running section", there is an optional [rc.d section](#running-headscale-in-the-background-with-rcd) describing how to make `headscale` run properly in a server environment. ## Install `headscale` @@ -77,16 +77,10 @@ describing how to make `headscale` run properly in a server environment. mkdir -p /etc/headscale - # Directory for Database, and other variable data (like certificates) + # Directory for database, and other variable data (like certificates) mkdir -p /var/lib/headscale ``` -1. Create an empty SQLite database: - - ```shell - touch /var/lib/headscale/db.sqlite - ``` - 1. Create a `headscale` configuration: ```shell @@ -135,7 +129,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale --user myfirstuser nodes register --key ``` ### Register machine using a pre authenticated key diff --git a/docs/running-headscale-sealos.md b/docs/running-headscale-sealos.md index 01aecb0e..1e3fe3ac 100644 --- a/docs/running-headscale-sealos.md +++ b/docs/running-headscale-sealos.md @@ -13,7 +13,7 @@ This documentation has the goal of showing a user how-to run `headscale` on Seal ## Running headscale server -1. Click the following prebuilt template(version [0.23.0-alpha2](https://github.com/juanfont/headscale/releases/tag/v0.23.0-alpha2)): +1. Click the following prebuilt template: [![](https://cdn.jsdelivr.net/gh/labring-actions/templates@main/Deploy-on-Sealos.svg)](https://cloud.sealos.io/?openapp=system-template%3FtemplateName%3Dheadscale) @@ -41,7 +41,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL To register a machine when running headscale in [Sealos](https://sealos.io), click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then take the headscale command: ```bash -headscale --user myfirstuser nodes register --key +headscale --user myfirstuser nodes register --key ``` ### Register machine using a pre authenticated key From f039caf1349d1c67d47ecd880d5efca1914f09b6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 4 Sep 2024 07:55:16 +0200 Subject: [PATCH 27/60] update godeps (#2098) --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 2 +- docs/remote-cli.md | 44 ++-- flake.nix | 33 ++- gen/go/headscale/v1/apikey.pb.go | 22 +- gen/go/headscale/v1/device.pb.go | 26 +- gen/go/headscale/v1/headscale.pb.go | 4 +- gen/go/headscale/v1/headscale.pb.gw.go | 105 ++------ gen/go/headscale/v1/headscale_grpc.pb.go | 150 +++++++----- gen/go/headscale/v1/node.pb.go | 46 ++-- gen/go/headscale/v1/policy.pb.go | 12 +- gen/go/headscale/v1/preauthkey.pb.go | 18 +- gen/go/headscale/v1/routes.pb.go | 26 +- gen/go/headscale/v1/user.pb.go | 26 +- .../headscale/v1/apikey.swagger.json | 1 + .../headscale/v1/device.swagger.json | 1 + .../headscale/v1/headscale.swagger.json | 28 ++- gen/openapiv2/headscale/v1/node.swagger.json | 1 + .../headscale/v1/policy.swagger.json | 1 + .../headscale/v1/preauthkey.swagger.json | 1 + .../headscale/v1/routes.swagger.json | 1 + gen/openapiv2/headscale/v1/user.swagger.json | 1 + go.mod | 97 ++++---- go.sum | 224 +++++++++--------- proto/headscale/v1/headscale.proto | 1 - proto/headscale/v1/policy.proto | 10 +- 26 files changed, 444 insertions(+), 439 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbb837fb..76982608 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,7 +45,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - `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. -- 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. - 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 18d7dfb8..4c3ca130 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,7 +9,7 @@ Headscale has a small maintainer team that tries to balance working on the proje When we work on issues ourselves, we develop first hand knowledge of the code and it makes it possible for us to maintain and own the code as the project develops. -Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. +Code contributions are seen as a positive thing. People enjoy and engage with our project, but it also comes with some challenges; we have to understand the code, we have to understand the feature, we might have to become familiar with external libraries or services and we think about security implications. All those steps are required during the reviewing process. After the code has been merged, the feature has to be maintained. Any changes reliant on external services must be updated and expanded accordingly. The review and day-1 maintenance adds a significant burden on the maintainers. Often we hope that the contributor will help out, but we found that most of the time, they disappear after their new feature was added. diff --git a/docs/remote-cli.md b/docs/remote-cli.md index 14423852..c641b789 100644 --- a/docs/remote-cli.md +++ b/docs/remote-cli.md @@ -47,40 +47,40 @@ headscale apikeys expire --prefix "" 3. Make `headscale` executable: - ```shell - chmod +x /usr/local/bin/headscale - ``` + ```shell + chmod +x /usr/local/bin/headscale + ``` 4. Configure the CLI through environment variables - ```shell - export HEADSCALE_CLI_ADDRESS=":" - export HEADSCALE_CLI_API_KEY="" - ``` + ```shell + export HEADSCALE_CLI_ADDRESS=":" + export HEADSCALE_CLI_API_KEY="" + ``` - for example: + for example: - ```shell - export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" - export HEADSCALE_CLI_API_KEY="abcde12345" - ``` + ```shell + export HEADSCALE_CLI_ADDRESS="headscale.example.com:50443" + export HEADSCALE_CLI_API_KEY="abcde12345" + ``` - This will tell the `headscale` binary to connect to a remote instance, instead of looking - for a local instance (which is what it does on the server). + This will tell the `headscale` binary to connect to a remote instance, instead of looking + for a local instance (which is what it does on the server). - The API key is needed to make sure that you are allowed to access the server. The key is _not_ - needed when running directly on the server, as the connection is local. + The API key is needed to make sure that you are allowed to access the server. The key is _not_ + needed when running directly on the server, as the connection is local. 5. Test the connection - Let us run the headscale command to verify that we can connect by listing our nodes: + Let us run the headscale command to verify that we can connect by listing our nodes: - ```shell - headscale nodes list - ``` + ```shell + headscale nodes list + ``` - You should now be able to see a list of your nodes from your workstation, and you can - now control the `headscale` server from your workstation. + You should now be able to see a list of your nodes from your workstation, and you can + now control the `headscale` server from your workstation. ## Behind a proxy diff --git a/flake.nix b/flake.nix index dbf4f38f..8e009c1f 100644 --- a/flake.nix +++ b/flake.nix @@ -20,8 +20,9 @@ { overlay = _: prev: let pkgs = nixpkgs.legacyPackages.${prev.system}; + buildGo = pkgs.buildGo123Module; in rec { - headscale = pkgs.buildGo123Module rec { + headscale = buildGo rec { pname = "headscale"; version = headscaleVersion; src = pkgs.lib.cleanSource self; @@ -31,30 +32,50 @@ # When updating go.mod or go.sum, a new sha will need to be calculated, # update this if you have a mismatch after doing a change to thos files. - vendorHash = "sha256-hmBRtMPqewg4oqu2bc9HtE3wdCdl5v9MoBOOCsjYlE8="; + vendorHash = "sha256-+8dOxPG/Q+wuHgRwwWqdphHOuop0W9dVyClyQuh7aRc="; subPackages = ["cmd/headscale"]; ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"]; }; - protoc-gen-grpc-gateway = pkgs.buildGoModule rec { + protoc-gen-grpc-gateway = buildGo rec { pname = "grpc-gateway"; - version = "2.19.1"; + version = "2.22.0"; src = pkgs.fetchFromGitHub { owner = "grpc-ecosystem"; repo = "grpc-gateway"; rev = "v${version}"; - sha256 = "sha256-CdGQpQfOSimeio8v1lZ7xzE/oAS2qFyu+uN+H9i7vpo="; + sha256 = "sha256-I1w3gfV06J8xG1xJ+XuMIGkV2/Ofszo7SCC+z4Xb6l4="; }; - vendorHash = "sha256-no7kZGpf/VOuceC3J+izGFQp5aMS3b+Rn+x4BFZ2zgs="; + vendorHash = "sha256-S4hcD5/BSGxM2qdJHMxOkxsJ5+Ks6m4lKHSS9+yZ17c="; nativeBuildInputs = [pkgs.installShellFiles]; subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"]; }; + + golangci-lint = prev.golangci-lint.override { + buildGoModule = buildGo; + }; + + goreleaser = prev.goreleaser.override { + buildGoModule = buildGo; + }; + + gotestsum = prev.gotestsum.override { + buildGoModule = buildGo; + }; + + gotests = prev.gotests.override { + buildGoModule = buildGo; + }; + + gofumpt = prev.gofumpt.override { + buildGoModule = buildGo; + }; }; } // flake-utils.lib.eachDefaultSystem diff --git a/gen/go/headscale/v1/apikey.pb.go b/gen/go/headscale/v1/apikey.pb.go index d1a5f555..e6263522 100644 --- a/gen/go/headscale/v1/apikey.pb.go +++ b/gen/go/headscale/v1/apikey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/apikey.proto @@ -512,7 +512,7 @@ func file_headscale_v1_apikey_proto_rawDescGZIP() []byte { } var file_headscale_v1_apikey_proto_msgTypes = make([]protoimpl.MessageInfo, 9) -var file_headscale_v1_apikey_proto_goTypes = []interface{}{ +var file_headscale_v1_apikey_proto_goTypes = []any{ (*ApiKey)(nil), // 0: headscale.v1.ApiKey (*CreateApiKeyRequest)(nil), // 1: headscale.v1.CreateApiKeyRequest (*CreateApiKeyResponse)(nil), // 2: headscale.v1.CreateApiKeyResponse @@ -543,7 +543,7 @@ func file_headscale_v1_apikey_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_apikey_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*ApiKey); i { case 0: return &v.state @@ -555,7 +555,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*CreateApiKeyRequest); i { case 0: return &v.state @@ -567,7 +567,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*CreateApiKeyResponse); i { case 0: return &v.state @@ -579,7 +579,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*ExpireApiKeyRequest); i { case 0: return &v.state @@ -591,7 +591,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*ExpireApiKeyResponse); i { case 0: return &v.state @@ -603,7 +603,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*ListApiKeysRequest); i { case 0: return &v.state @@ -615,7 +615,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*ListApiKeysResponse); i { case 0: return &v.state @@ -627,7 +627,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*DeleteApiKeyRequest); i { case 0: return &v.state @@ -639,7 +639,7 @@ func file_headscale_v1_apikey_proto_init() { return nil } } - file_headscale_v1_apikey_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_apikey_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*DeleteApiKeyResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index 40e2e24f..66c31441 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/device.proto @@ -925,7 +925,7 @@ func file_headscale_v1_device_proto_rawDescGZIP() []byte { } var file_headscale_v1_device_proto_msgTypes = make([]protoimpl.MessageInfo, 12) -var file_headscale_v1_device_proto_goTypes = []interface{}{ +var file_headscale_v1_device_proto_goTypes = []any{ (*Latency)(nil), // 0: headscale.v1.Latency (*ClientSupports)(nil), // 1: headscale.v1.ClientSupports (*ClientConnectivity)(nil), // 2: headscale.v1.ClientConnectivity @@ -961,7 +961,7 @@ func file_headscale_v1_device_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_device_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Latency); i { case 0: return &v.state @@ -973,7 +973,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*ClientSupports); i { case 0: return &v.state @@ -985,7 +985,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*ClientConnectivity); i { case 0: return &v.state @@ -997,7 +997,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceRequest); i { case 0: return &v.state @@ -1009,7 +1009,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceResponse); i { case 0: return &v.state @@ -1021,7 +1021,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*DeleteDeviceRequest); i { case 0: return &v.state @@ -1033,7 +1033,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*DeleteDeviceResponse); i { case 0: return &v.state @@ -1045,7 +1045,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceRoutesRequest); i { case 0: return &v.state @@ -1057,7 +1057,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*GetDeviceRoutesResponse); i { case 0: return &v.state @@ -1069,7 +1069,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*EnableDeviceRoutesRequest); i { case 0: return &v.state @@ -1081,7 +1081,7 @@ func file_headscale_v1_device_proto_init() { return nil } } - file_headscale_v1_device_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_device_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*EnableDeviceRoutesResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index 63e7d536..d6751864 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/headscale.proto @@ -257,7 +257,7 @@ var file_headscale_v1_headscale_proto_rawDesc = []byte{ 0x33, } -var file_headscale_v1_headscale_proto_goTypes = []interface{}{ +var file_headscale_v1_headscale_proto_goTypes = []any{ (*GetUserRequest)(nil), // 0: headscale.v1.GetUserRequest (*CreateUserRequest)(nil), // 1: headscale.v1.CreateUserRequest (*RenameUserRequest)(nil), // 2: headscale.v1.RenameUserRequest diff --git a/gen/go/headscale/v1/headscale.pb.gw.go b/gen/go/headscale/v1/headscale.pb.gw.go index 98c6039b..59a98ce3 100644 --- a/gen/go/headscale/v1/headscale.pb.gw.go +++ b/gen/go/headscale/v1/headscale.pb.gw.go @@ -87,11 +87,7 @@ func request_HeadscaleService_CreateUser_0(ctx context.Context, marshaler runtim var protoReq CreateUserRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -104,11 +100,7 @@ func local_request_HeadscaleService_CreateUser_0(ctx context.Context, marshaler var protoReq CreateUserRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -263,11 +255,7 @@ func request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, marshaler var protoReq CreatePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -280,11 +268,7 @@ func local_request_HeadscaleService_CreatePreAuthKey_0(ctx context.Context, mars var protoReq CreatePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -297,11 +281,7 @@ func request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, marshaler var protoReq ExpirePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -314,11 +294,7 @@ func local_request_HeadscaleService_ExpirePreAuthKey_0(ctx context.Context, mars var protoReq ExpirePreAuthKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -367,11 +343,7 @@ func request_HeadscaleService_DebugCreateNode_0(ctx context.Context, marshaler r var protoReq DebugCreateNodeRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -384,11 +356,7 @@ func local_request_HeadscaleService_DebugCreateNode_0(ctx context.Context, marsh var protoReq DebugCreateNodeRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -453,11 +421,7 @@ func request_HeadscaleService_SetTags_0(ctx context.Context, marshaler runtime.M var protoReq SetTagsRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -487,11 +451,7 @@ func local_request_HeadscaleService_SetTags_0(ctx context.Context, marshaler run var protoReq SetTagsRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1101,11 +1061,7 @@ func request_HeadscaleService_CreateApiKey_0(ctx context.Context, marshaler runt var protoReq CreateApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1118,11 +1074,7 @@ func local_request_HeadscaleService_CreateApiKey_0(ctx context.Context, marshale var protoReq CreateApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1135,11 +1087,7 @@ func request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marshaler runt var protoReq ExpireApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1152,11 +1100,7 @@ func local_request_HeadscaleService_ExpireApiKey_0(ctx context.Context, marshale var protoReq ExpireApiKeyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1257,11 +1201,7 @@ func request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler runtime var protoReq SetPolicyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1274,11 +1214,7 @@ func local_request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler r var protoReq SetPolicyRequest var metadata runtime.ServerMetadata - newReader, berr := utilities.IOReaderFactory(req.Body) - if berr != nil { - return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) - } - if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && err != io.EOF { return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) } @@ -1291,6 +1227,7 @@ func local_request_HeadscaleService_SetPolicy_0(ctx context.Context, marshaler r // UnaryRPC :call HeadscaleServiceServer directly. // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. // Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterHeadscaleServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server HeadscaleServiceServer) error { mux.Handle("GET", pattern_HeadscaleService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { @@ -2024,21 +1961,21 @@ func RegisterHeadscaleServiceHandlerServer(ctx context.Context, mux *runtime.Ser // RegisterHeadscaleServiceHandlerFromEndpoint is same as RegisterHeadscaleServiceHandler but // automatically dials to "endpoint" and closes the connection when "ctx" gets done. func RegisterHeadscaleServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { - conn, err := grpc.Dial(endpoint, opts...) + conn, err := grpc.NewClient(endpoint, opts...) if err != nil { return err } defer func() { if err != nil { if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } return } go func() { <-ctx.Done() if cerr := conn.Close(); cerr != nil { - grpclog.Infof("Failed to close conn to %s: %v", endpoint, cerr) + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) } }() }() @@ -2056,7 +1993,7 @@ func RegisterHeadscaleServiceHandler(ctx context.Context, mux *runtime.ServeMux, // to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "HeadscaleServiceClient". // Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "HeadscaleServiceClient" // doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in -// "HeadscaleServiceClient" to call the correct interceptors. +// "HeadscaleServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. func RegisterHeadscaleServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client HeadscaleServiceClient) error { mux.Handle("GET", pattern_HeadscaleService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { diff --git a/gen/go/headscale/v1/headscale_grpc.pb.go b/gen/go/headscale/v1/headscale_grpc.pb.go index df9cf197..d57aa92e 100644 --- a/gen/go/headscale/v1/headscale_grpc.pb.go +++ b/gen/go/headscale/v1/headscale_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.2.0 +// - protoc-gen-go-grpc v1.3.0 // - protoc (unknown) // source: headscale/v1/headscale.proto @@ -18,6 +18,38 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 +const ( + HeadscaleService_GetUser_FullMethodName = "/headscale.v1.HeadscaleService/GetUser" + HeadscaleService_CreateUser_FullMethodName = "/headscale.v1.HeadscaleService/CreateUser" + HeadscaleService_RenameUser_FullMethodName = "/headscale.v1.HeadscaleService/RenameUser" + HeadscaleService_DeleteUser_FullMethodName = "/headscale.v1.HeadscaleService/DeleteUser" + HeadscaleService_ListUsers_FullMethodName = "/headscale.v1.HeadscaleService/ListUsers" + HeadscaleService_CreatePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/CreatePreAuthKey" + HeadscaleService_ExpirePreAuthKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpirePreAuthKey" + HeadscaleService_ListPreAuthKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListPreAuthKeys" + HeadscaleService_DebugCreateNode_FullMethodName = "/headscale.v1.HeadscaleService/DebugCreateNode" + HeadscaleService_GetNode_FullMethodName = "/headscale.v1.HeadscaleService/GetNode" + HeadscaleService_SetTags_FullMethodName = "/headscale.v1.HeadscaleService/SetTags" + HeadscaleService_RegisterNode_FullMethodName = "/headscale.v1.HeadscaleService/RegisterNode" + HeadscaleService_DeleteNode_FullMethodName = "/headscale.v1.HeadscaleService/DeleteNode" + HeadscaleService_ExpireNode_FullMethodName = "/headscale.v1.HeadscaleService/ExpireNode" + HeadscaleService_RenameNode_FullMethodName = "/headscale.v1.HeadscaleService/RenameNode" + HeadscaleService_ListNodes_FullMethodName = "/headscale.v1.HeadscaleService/ListNodes" + HeadscaleService_MoveNode_FullMethodName = "/headscale.v1.HeadscaleService/MoveNode" + HeadscaleService_BackfillNodeIPs_FullMethodName = "/headscale.v1.HeadscaleService/BackfillNodeIPs" + HeadscaleService_GetRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetRoutes" + HeadscaleService_EnableRoute_FullMethodName = "/headscale.v1.HeadscaleService/EnableRoute" + HeadscaleService_DisableRoute_FullMethodName = "/headscale.v1.HeadscaleService/DisableRoute" + HeadscaleService_GetNodeRoutes_FullMethodName = "/headscale.v1.HeadscaleService/GetNodeRoutes" + HeadscaleService_DeleteRoute_FullMethodName = "/headscale.v1.HeadscaleService/DeleteRoute" + HeadscaleService_CreateApiKey_FullMethodName = "/headscale.v1.HeadscaleService/CreateApiKey" + HeadscaleService_ExpireApiKey_FullMethodName = "/headscale.v1.HeadscaleService/ExpireApiKey" + HeadscaleService_ListApiKeys_FullMethodName = "/headscale.v1.HeadscaleService/ListApiKeys" + HeadscaleService_DeleteApiKey_FullMethodName = "/headscale.v1.HeadscaleService/DeleteApiKey" + HeadscaleService_GetPolicy_FullMethodName = "/headscale.v1.HeadscaleService/GetPolicy" + HeadscaleService_SetPolicy_FullMethodName = "/headscale.v1.HeadscaleService/SetPolicy" +) + // HeadscaleServiceClient is the client API for HeadscaleService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -69,7 +101,7 @@ func NewHeadscaleServiceClient(cc grpc.ClientConnInterface) HeadscaleServiceClie func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*GetUserResponse, error) { out := new(GetUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -78,7 +110,7 @@ func (c *headscaleServiceClient) GetUser(ctx context.Context, in *GetUserRequest func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*CreateUserResponse, error) { out := new(CreateUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreateUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -87,7 +119,7 @@ func (c *headscaleServiceClient) CreateUser(ctx context.Context, in *CreateUserR func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserRequest, opts ...grpc.CallOption) (*RenameUserResponse, error) { out := new(RenameUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RenameUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -96,7 +128,7 @@ func (c *headscaleServiceClient) RenameUser(ctx context.Context, in *RenameUserR func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*DeleteUserResponse, error) { out := new(DeleteUserResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteUser", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteUser_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -105,7 +137,7 @@ func (c *headscaleServiceClient) DeleteUser(ctx context.Context, in *DeleteUserR func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { out := new(ListUsersResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListUsers", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListUsers_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -114,7 +146,7 @@ func (c *headscaleServiceClient) ListUsers(ctx context.Context, in *ListUsersReq func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*CreatePreAuthKeyResponse, error) { out := new(CreatePreAuthKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreatePreAuthKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreatePreAuthKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -123,7 +155,7 @@ func (c *headscaleServiceClient) CreatePreAuthKey(ctx context.Context, in *Creat func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*ExpirePreAuthKeyResponse, error) { out := new(ExpirePreAuthKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpirePreAuthKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpirePreAuthKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -132,7 +164,7 @@ func (c *headscaleServiceClient) ExpirePreAuthKey(ctx context.Context, in *Expir func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPreAuthKeysRequest, opts ...grpc.CallOption) (*ListPreAuthKeysResponse, error) { out := new(ListPreAuthKeysResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListPreAuthKeys", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListPreAuthKeys_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -141,7 +173,7 @@ func (c *headscaleServiceClient) ListPreAuthKeys(ctx context.Context, in *ListPr func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, in *DebugCreateNodeRequest, opts ...grpc.CallOption) (*DebugCreateNodeResponse, error) { out := new(DebugCreateNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DebugCreateNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DebugCreateNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -150,7 +182,7 @@ func (c *headscaleServiceClient) DebugCreateNode(ctx context.Context, in *DebugC func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetNodeRequest, opts ...grpc.CallOption) (*GetNodeResponse, error) { out := new(GetNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -159,7 +191,7 @@ func (c *headscaleServiceClient) GetNode(ctx context.Context, in *GetNodeRequest func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest, opts ...grpc.CallOption) (*SetTagsResponse, error) { out := new(SetTagsResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetTags", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_SetTags_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -168,7 +200,7 @@ func (c *headscaleServiceClient) SetTags(ctx context.Context, in *SetTagsRequest func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in *RegisterNodeRequest, opts ...grpc.CallOption) (*RegisterNodeResponse, error) { out := new(RegisterNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RegisterNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RegisterNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -177,7 +209,7 @@ func (c *headscaleServiceClient) RegisterNode(ctx context.Context, in *RegisterN func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *DeleteNodeRequest, opts ...grpc.CallOption) (*DeleteNodeResponse, error) { out := new(DeleteNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -186,7 +218,7 @@ func (c *headscaleServiceClient) DeleteNode(ctx context.Context, in *DeleteNodeR func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *ExpireNodeRequest, opts ...grpc.CallOption) (*ExpireNodeResponse, error) { out := new(ExpireNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpireNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -195,7 +227,7 @@ func (c *headscaleServiceClient) ExpireNode(ctx context.Context, in *ExpireNodeR func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *RenameNodeRequest, opts ...grpc.CallOption) (*RenameNodeResponse, error) { out := new(RenameNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/RenameNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_RenameNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -204,7 +236,7 @@ func (c *headscaleServiceClient) RenameNode(ctx context.Context, in *RenameNodeR func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *ListNodesRequest, opts ...grpc.CallOption) (*ListNodesResponse, error) { out := new(ListNodesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListNodes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListNodes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -213,7 +245,7 @@ func (c *headscaleServiceClient) ListNodes(ctx context.Context, in *ListNodesReq func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeRequest, opts ...grpc.CallOption) (*MoveNodeResponse, error) { out := new(MoveNodeResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/MoveNode", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_MoveNode_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -222,7 +254,7 @@ func (c *headscaleServiceClient) MoveNode(ctx context.Context, in *MoveNodeReque func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *BackfillNodeIPsRequest, opts ...grpc.CallOption) (*BackfillNodeIPsResponse, error) { out := new(BackfillNodeIPsResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/BackfillNodeIPs", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_BackfillNodeIPs_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -231,7 +263,7 @@ func (c *headscaleServiceClient) BackfillNodeIPs(ctx context.Context, in *Backfi func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesRequest, opts ...grpc.CallOption) (*GetRoutesResponse, error) { out := new(GetRoutesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetRoutes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetRoutes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -240,7 +272,7 @@ func (c *headscaleServiceClient) GetRoutes(ctx context.Context, in *GetRoutesReq func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRouteRequest, opts ...grpc.CallOption) (*EnableRouteResponse, error) { out := new(EnableRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/EnableRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_EnableRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -249,7 +281,7 @@ func (c *headscaleServiceClient) EnableRoute(ctx context.Context, in *EnableRout func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRouteRequest, opts ...grpc.CallOption) (*DisableRouteResponse, error) { out := new(DisableRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DisableRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DisableRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -258,7 +290,7 @@ func (c *headscaleServiceClient) DisableRoute(ctx context.Context, in *DisableRo func (c *headscaleServiceClient) GetNodeRoutes(ctx context.Context, in *GetNodeRoutesRequest, opts ...grpc.CallOption) (*GetNodeRoutesResponse, error) { out := new(GetNodeRoutesResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetNodeRoutes", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetNodeRoutes_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -267,7 +299,7 @@ func (c *headscaleServiceClient) GetNodeRoutes(ctx context.Context, in *GetNodeR func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRouteRequest, opts ...grpc.CallOption) (*DeleteRouteResponse, error) { out := new(DeleteRouteResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteRoute", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteRoute_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -276,7 +308,7 @@ func (c *headscaleServiceClient) DeleteRoute(ctx context.Context, in *DeleteRout func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApiKeyRequest, opts ...grpc.CallOption) (*CreateApiKeyResponse, error) { out := new(CreateApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/CreateApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_CreateApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -285,7 +317,7 @@ func (c *headscaleServiceClient) CreateApiKey(ctx context.Context, in *CreateApi func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApiKeyRequest, opts ...grpc.CallOption) (*ExpireApiKeyResponse, error) { out := new(ExpireApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ExpireApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ExpireApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -294,7 +326,7 @@ func (c *headscaleServiceClient) ExpireApiKey(ctx context.Context, in *ExpireApi func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKeysRequest, opts ...grpc.CallOption) (*ListApiKeysResponse, error) { out := new(ListApiKeysResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/ListApiKeys", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_ListApiKeys_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -303,7 +335,7 @@ func (c *headscaleServiceClient) ListApiKeys(ctx context.Context, in *ListApiKey func (c *headscaleServiceClient) DeleteApiKey(ctx context.Context, in *DeleteApiKeyRequest, opts ...grpc.CallOption) (*DeleteApiKeyResponse, error) { out := new(DeleteApiKeyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/DeleteApiKey", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_DeleteApiKey_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -312,7 +344,7 @@ func (c *headscaleServiceClient) DeleteApiKey(ctx context.Context, in *DeleteApi func (c *headscaleServiceClient) GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error) { out := new(GetPolicyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/GetPolicy", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_GetPolicy_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -321,7 +353,7 @@ func (c *headscaleServiceClient) GetPolicy(ctx context.Context, in *GetPolicyReq func (c *headscaleServiceClient) SetPolicy(ctx context.Context, in *SetPolicyRequest, opts ...grpc.CallOption) (*SetPolicyResponse, error) { out := new(SetPolicyResponse) - err := c.cc.Invoke(ctx, "/headscale.v1.HeadscaleService/SetPolicy", in, out, opts...) + err := c.cc.Invoke(ctx, HeadscaleService_SetPolicy_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -484,7 +516,7 @@ func _HeadscaleService_GetUser_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetUser", + FullMethod: HeadscaleService_GetUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetUser(ctx, req.(*GetUserRequest)) @@ -502,7 +534,7 @@ func _HeadscaleService_CreateUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreateUser", + FullMethod: HeadscaleService_CreateUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) @@ -520,7 +552,7 @@ func _HeadscaleService_RenameUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RenameUser", + FullMethod: HeadscaleService_RenameUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameUser(ctx, req.(*RenameUserRequest)) @@ -538,7 +570,7 @@ func _HeadscaleService_DeleteUser_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteUser", + FullMethod: HeadscaleService_DeleteUser_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) @@ -556,7 +588,7 @@ func _HeadscaleService_ListUsers_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListUsers", + FullMethod: HeadscaleService_ListUsers_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) @@ -574,7 +606,7 @@ func _HeadscaleService_CreatePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreatePreAuthKey", + FullMethod: HeadscaleService_CreatePreAuthKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreatePreAuthKey(ctx, req.(*CreatePreAuthKeyRequest)) @@ -592,7 +624,7 @@ func _HeadscaleService_ExpirePreAuthKey_Handler(srv interface{}, ctx context.Con } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpirePreAuthKey", + FullMethod: HeadscaleService_ExpirePreAuthKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpirePreAuthKey(ctx, req.(*ExpirePreAuthKeyRequest)) @@ -610,7 +642,7 @@ func _HeadscaleService_ListPreAuthKeys_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListPreAuthKeys", + FullMethod: HeadscaleService_ListPreAuthKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListPreAuthKeys(ctx, req.(*ListPreAuthKeysRequest)) @@ -628,7 +660,7 @@ func _HeadscaleService_DebugCreateNode_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DebugCreateNode", + FullMethod: HeadscaleService_DebugCreateNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DebugCreateNode(ctx, req.(*DebugCreateNodeRequest)) @@ -646,7 +678,7 @@ func _HeadscaleService_GetNode_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetNode", + FullMethod: HeadscaleService_GetNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetNode(ctx, req.(*GetNodeRequest)) @@ -664,7 +696,7 @@ func _HeadscaleService_SetTags_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/SetTags", + FullMethod: HeadscaleService_SetTags_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).SetTags(ctx, req.(*SetTagsRequest)) @@ -682,7 +714,7 @@ func _HeadscaleService_RegisterNode_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RegisterNode", + FullMethod: HeadscaleService_RegisterNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RegisterNode(ctx, req.(*RegisterNodeRequest)) @@ -700,7 +732,7 @@ func _HeadscaleService_DeleteNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteNode", + FullMethod: HeadscaleService_DeleteNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteNode(ctx, req.(*DeleteNodeRequest)) @@ -718,7 +750,7 @@ func _HeadscaleService_ExpireNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpireNode", + FullMethod: HeadscaleService_ExpireNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireNode(ctx, req.(*ExpireNodeRequest)) @@ -736,7 +768,7 @@ func _HeadscaleService_RenameNode_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/RenameNode", + FullMethod: HeadscaleService_RenameNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).RenameNode(ctx, req.(*RenameNodeRequest)) @@ -754,7 +786,7 @@ func _HeadscaleService_ListNodes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListNodes", + FullMethod: HeadscaleService_ListNodes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListNodes(ctx, req.(*ListNodesRequest)) @@ -772,7 +804,7 @@ func _HeadscaleService_MoveNode_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/MoveNode", + FullMethod: HeadscaleService_MoveNode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).MoveNode(ctx, req.(*MoveNodeRequest)) @@ -790,7 +822,7 @@ func _HeadscaleService_BackfillNodeIPs_Handler(srv interface{}, ctx context.Cont } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/BackfillNodeIPs", + FullMethod: HeadscaleService_BackfillNodeIPs_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).BackfillNodeIPs(ctx, req.(*BackfillNodeIPsRequest)) @@ -808,7 +840,7 @@ func _HeadscaleService_GetRoutes_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetRoutes", + FullMethod: HeadscaleService_GetRoutes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetRoutes(ctx, req.(*GetRoutesRequest)) @@ -826,7 +858,7 @@ func _HeadscaleService_EnableRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/EnableRoute", + FullMethod: HeadscaleService_EnableRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).EnableRoute(ctx, req.(*EnableRouteRequest)) @@ -844,7 +876,7 @@ func _HeadscaleService_DisableRoute_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DisableRoute", + FullMethod: HeadscaleService_DisableRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DisableRoute(ctx, req.(*DisableRouteRequest)) @@ -862,7 +894,7 @@ func _HeadscaleService_GetNodeRoutes_Handler(srv interface{}, ctx context.Contex } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetNodeRoutes", + FullMethod: HeadscaleService_GetNodeRoutes_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetNodeRoutes(ctx, req.(*GetNodeRoutesRequest)) @@ -880,7 +912,7 @@ func _HeadscaleService_DeleteRoute_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteRoute", + FullMethod: HeadscaleService_DeleteRoute_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteRoute(ctx, req.(*DeleteRouteRequest)) @@ -898,7 +930,7 @@ func _HeadscaleService_CreateApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/CreateApiKey", + FullMethod: HeadscaleService_CreateApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).CreateApiKey(ctx, req.(*CreateApiKeyRequest)) @@ -916,7 +948,7 @@ func _HeadscaleService_ExpireApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ExpireApiKey", + FullMethod: HeadscaleService_ExpireApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ExpireApiKey(ctx, req.(*ExpireApiKeyRequest)) @@ -934,7 +966,7 @@ func _HeadscaleService_ListApiKeys_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/ListApiKeys", + FullMethod: HeadscaleService_ListApiKeys_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).ListApiKeys(ctx, req.(*ListApiKeysRequest)) @@ -952,7 +984,7 @@ func _HeadscaleService_DeleteApiKey_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/DeleteApiKey", + FullMethod: HeadscaleService_DeleteApiKey_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).DeleteApiKey(ctx, req.(*DeleteApiKeyRequest)) @@ -970,7 +1002,7 @@ func _HeadscaleService_GetPolicy_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/GetPolicy", + FullMethod: HeadscaleService_GetPolicy_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).GetPolicy(ctx, req.(*GetPolicyRequest)) @@ -988,7 +1020,7 @@ func _HeadscaleService_SetPolicy_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/headscale.v1.HeadscaleService/SetPolicy", + FullMethod: HeadscaleService_SetPolicy_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(HeadscaleServiceServer).SetPolicy(ctx, req.(*SetPolicyRequest)) diff --git a/gen/go/headscale/v1/node.pb.go b/gen/go/headscale/v1/node.pb.go index b961ca73..61ed4064 100644 --- a/gen/go/headscale/v1/node.pb.go +++ b/gen/go/headscale/v1/node.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/node.proto @@ -1389,7 +1389,7 @@ func file_headscale_v1_node_proto_rawDescGZIP() []byte { var file_headscale_v1_node_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_headscale_v1_node_proto_msgTypes = make([]protoimpl.MessageInfo, 21) -var file_headscale_v1_node_proto_goTypes = []interface{}{ +var file_headscale_v1_node_proto_goTypes = []any{ (RegisterMethod)(0), // 0: headscale.v1.RegisterMethod (*Node)(nil), // 1: headscale.v1.Node (*RegisterNodeRequest)(nil), // 2: headscale.v1.RegisterNodeRequest @@ -1446,7 +1446,7 @@ func file_headscale_v1_node_proto_init() { file_headscale_v1_preauthkey_proto_init() file_headscale_v1_user_proto_init() if !protoimpl.UnsafeEnabled { - file_headscale_v1_node_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Node); i { case 0: return &v.state @@ -1458,7 +1458,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*RegisterNodeRequest); i { case 0: return &v.state @@ -1470,7 +1470,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*RegisterNodeResponse); i { case 0: return &v.state @@ -1482,7 +1482,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*GetNodeRequest); i { case 0: return &v.state @@ -1494,7 +1494,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*GetNodeResponse); i { case 0: return &v.state @@ -1506,7 +1506,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*SetTagsRequest); i { case 0: return &v.state @@ -1518,7 +1518,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*SetTagsResponse); i { case 0: return &v.state @@ -1530,7 +1530,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*DeleteNodeRequest); i { case 0: return &v.state @@ -1542,7 +1542,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*DeleteNodeResponse); i { case 0: return &v.state @@ -1554,7 +1554,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*ExpireNodeRequest); i { case 0: return &v.state @@ -1566,7 +1566,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*ExpireNodeResponse); i { case 0: return &v.state @@ -1578,7 +1578,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[11].Exporter = func(v any, i int) any { switch v := v.(*RenameNodeRequest); i { case 0: return &v.state @@ -1590,7 +1590,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[12].Exporter = func(v any, i int) any { switch v := v.(*RenameNodeResponse); i { case 0: return &v.state @@ -1602,7 +1602,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[13].Exporter = func(v any, i int) any { switch v := v.(*ListNodesRequest); i { case 0: return &v.state @@ -1614,7 +1614,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[14].Exporter = func(v any, i int) any { switch v := v.(*ListNodesResponse); i { case 0: return &v.state @@ -1626,7 +1626,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[15].Exporter = func(v any, i int) any { switch v := v.(*MoveNodeRequest); i { case 0: return &v.state @@ -1638,7 +1638,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[16].Exporter = func(v any, i int) any { switch v := v.(*MoveNodeResponse); i { case 0: return &v.state @@ -1650,7 +1650,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[17].Exporter = func(v any, i int) any { switch v := v.(*DebugCreateNodeRequest); i { case 0: return &v.state @@ -1662,7 +1662,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[18].Exporter = func(v any, i int) any { switch v := v.(*DebugCreateNodeResponse); i { case 0: return &v.state @@ -1674,7 +1674,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[19].Exporter = func(v any, i int) any { switch v := v.(*BackfillNodeIPsRequest); i { case 0: return &v.state @@ -1686,7 +1686,7 @@ func file_headscale_v1_node_proto_init() { return nil } } - file_headscale_v1_node_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_node_proto_msgTypes[20].Exporter = func(v any, i int) any { switch v := v.(*BackfillNodeIPsResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/policy.pb.go b/gen/go/headscale/v1/policy.pb.go index 31ecffdf..62a079be 100644 --- a/gen/go/headscale/v1/policy.pb.go +++ b/gen/go/headscale/v1/policy.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/policy.proto @@ -259,7 +259,7 @@ func file_headscale_v1_policy_proto_rawDescGZIP() []byte { } var file_headscale_v1_policy_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_headscale_v1_policy_proto_goTypes = []interface{}{ +var file_headscale_v1_policy_proto_goTypes = []any{ (*SetPolicyRequest)(nil), // 0: headscale.v1.SetPolicyRequest (*SetPolicyResponse)(nil), // 1: headscale.v1.SetPolicyResponse (*GetPolicyRequest)(nil), // 2: headscale.v1.GetPolicyRequest @@ -282,7 +282,7 @@ func file_headscale_v1_policy_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_policy_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*SetPolicyRequest); i { case 0: return &v.state @@ -294,7 +294,7 @@ func file_headscale_v1_policy_proto_init() { return nil } } - file_headscale_v1_policy_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*SetPolicyResponse); i { case 0: return &v.state @@ -306,7 +306,7 @@ func file_headscale_v1_policy_proto_init() { return nil } } - file_headscale_v1_policy_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*GetPolicyRequest); i { case 0: return &v.state @@ -318,7 +318,7 @@ func file_headscale_v1_policy_proto_init() { return nil } } - file_headscale_v1_policy_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_policy_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*GetPolicyResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 35a0dfe0..ede617f2 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/preauthkey.proto @@ -522,7 +522,7 @@ func file_headscale_v1_preauthkey_proto_rawDescGZIP() []byte { } var file_headscale_v1_preauthkey_proto_msgTypes = make([]protoimpl.MessageInfo, 7) -var file_headscale_v1_preauthkey_proto_goTypes = []interface{}{ +var file_headscale_v1_preauthkey_proto_goTypes = []any{ (*PreAuthKey)(nil), // 0: headscale.v1.PreAuthKey (*CreatePreAuthKeyRequest)(nil), // 1: headscale.v1.CreatePreAuthKeyRequest (*CreatePreAuthKeyResponse)(nil), // 2: headscale.v1.CreatePreAuthKeyResponse @@ -551,7 +551,7 @@ func file_headscale_v1_preauthkey_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_preauthkey_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*PreAuthKey); i { case 0: return &v.state @@ -563,7 +563,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*CreatePreAuthKeyRequest); i { case 0: return &v.state @@ -575,7 +575,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*CreatePreAuthKeyResponse); i { case 0: return &v.state @@ -587,7 +587,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*ExpirePreAuthKeyRequest); i { case 0: return &v.state @@ -599,7 +599,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*ExpirePreAuthKeyResponse); i { case 0: return &v.state @@ -611,7 +611,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*ListPreAuthKeysRequest); i { case 0: return &v.state @@ -623,7 +623,7 @@ func file_headscale_v1_preauthkey_proto_init() { return nil } } - file_headscale_v1_preauthkey_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_preauthkey_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*ListPreAuthKeysResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index d2273047..76806db8 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/routes.proto @@ -643,7 +643,7 @@ func file_headscale_v1_routes_proto_rawDescGZIP() []byte { } var file_headscale_v1_routes_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_headscale_v1_routes_proto_goTypes = []interface{}{ +var file_headscale_v1_routes_proto_goTypes = []any{ (*Route)(nil), // 0: headscale.v1.Route (*GetRoutesRequest)(nil), // 1: headscale.v1.GetRoutesRequest (*GetRoutesResponse)(nil), // 2: headscale.v1.GetRoutesResponse @@ -679,7 +679,7 @@ func file_headscale_v1_routes_proto_init() { } file_headscale_v1_node_proto_init() if !protoimpl.UnsafeEnabled { - file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*Route); i { case 0: return &v.state @@ -691,7 +691,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*GetRoutesRequest); i { case 0: return &v.state @@ -703,7 +703,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*GetRoutesResponse); i { case 0: return &v.state @@ -715,7 +715,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*EnableRouteRequest); i { case 0: return &v.state @@ -727,7 +727,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*EnableRouteResponse); i { case 0: return &v.state @@ -739,7 +739,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*DisableRouteRequest); i { case 0: return &v.state @@ -751,7 +751,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*DisableRouteResponse); i { case 0: return &v.state @@ -763,7 +763,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*GetNodeRoutesRequest); i { case 0: return &v.state @@ -775,7 +775,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*GetNodeRoutesResponse); i { case 0: return &v.state @@ -787,7 +787,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*DeleteRouteRequest); i { case 0: return &v.state @@ -799,7 +799,7 @@ func file_headscale_v1_routes_proto_init() { return nil } } - file_headscale_v1_routes_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_routes_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*DeleteRouteResponse); i { case 0: return &v.state diff --git a/gen/go/headscale/v1/user.pb.go b/gen/go/headscale/v1/user.pb.go index 17cb4b54..ff1a5689 100644 --- a/gen/go/headscale/v1/user.pb.go +++ b/gen/go/headscale/v1/user.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.34.2 // protoc (unknown) // source: headscale/v1/user.proto @@ -607,7 +607,7 @@ func file_headscale_v1_user_proto_rawDescGZIP() []byte { } var file_headscale_v1_user_proto_msgTypes = make([]protoimpl.MessageInfo, 11) -var file_headscale_v1_user_proto_goTypes = []interface{}{ +var file_headscale_v1_user_proto_goTypes = []any{ (*User)(nil), // 0: headscale.v1.User (*GetUserRequest)(nil), // 1: headscale.v1.GetUserRequest (*GetUserResponse)(nil), // 2: headscale.v1.GetUserResponse @@ -640,7 +640,7 @@ func file_headscale_v1_user_proto_init() { return } if !protoimpl.UnsafeEnabled { - file_headscale_v1_user_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[0].Exporter = func(v any, i int) any { switch v := v.(*User); i { case 0: return &v.state @@ -652,7 +652,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[1].Exporter = func(v any, i int) any { switch v := v.(*GetUserRequest); i { case 0: return &v.state @@ -664,7 +664,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[2].Exporter = func(v any, i int) any { switch v := v.(*GetUserResponse); i { case 0: return &v.state @@ -676,7 +676,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[3].Exporter = func(v any, i int) any { switch v := v.(*CreateUserRequest); i { case 0: return &v.state @@ -688,7 +688,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[4].Exporter = func(v any, i int) any { switch v := v.(*CreateUserResponse); i { case 0: return &v.state @@ -700,7 +700,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[5].Exporter = func(v any, i int) any { switch v := v.(*RenameUserRequest); i { case 0: return &v.state @@ -712,7 +712,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[6].Exporter = func(v any, i int) any { switch v := v.(*RenameUserResponse); i { case 0: return &v.state @@ -724,7 +724,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[7].Exporter = func(v any, i int) any { switch v := v.(*DeleteUserRequest); i { case 0: return &v.state @@ -736,7 +736,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[8].Exporter = func(v any, i int) any { switch v := v.(*DeleteUserResponse); i { case 0: return &v.state @@ -748,7 +748,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[9].Exporter = func(v any, i int) any { switch v := v.(*ListUsersRequest); i { case 0: return &v.state @@ -760,7 +760,7 @@ func file_headscale_v1_user_proto_init() { return nil } } - file_headscale_v1_user_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + file_headscale_v1_user_proto_msgTypes[10].Exporter = func(v any, i int) any { switch v := v.(*ListUsersResponse); i { case 0: return &v.state diff --git a/gen/openapiv2/headscale/v1/apikey.swagger.json b/gen/openapiv2/headscale/v1/apikey.swagger.json index 0d4ebbe9..8c8596a9 100644 --- a/gen/openapiv2/headscale/v1/apikey.swagger.json +++ b/gen/openapiv2/headscale/v1/apikey.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/device.swagger.json b/gen/openapiv2/headscale/v1/device.swagger.json index 5360527a..99d20deb 100644 --- a/gen/openapiv2/headscale/v1/device.swagger.json +++ b/gen/openapiv2/headscale/v1/device.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 9c1cf0e9..9530ea4d 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -449,15 +449,7 @@ "in": "body", "required": true, "schema": { - "type": "object", - "properties": { - "tags": { - "type": "array", - "items": { - "type": "string" - } - } - } + "$ref": "#/definitions/HeadscaleServiceSetTagsBody" } } ], @@ -914,6 +906,17 @@ } }, "definitions": { + "HeadscaleServiceSetTagsBody": { + "type": "object", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, "protobufAny": { "type": "object", "properties": { @@ -936,6 +939,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } @@ -1134,6 +1138,7 @@ "routes": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1Route" } } @@ -1157,6 +1162,7 @@ "routes": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1Route" } } @@ -1176,6 +1182,7 @@ "apiKeys": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1ApiKey" } } @@ -1187,6 +1194,7 @@ "nodes": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1Node" } } @@ -1198,6 +1206,7 @@ "preAuthKeys": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1PreAuthKey" } } @@ -1209,6 +1218,7 @@ "users": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/v1User" } } diff --git a/gen/openapiv2/headscale/v1/node.swagger.json b/gen/openapiv2/headscale/v1/node.swagger.json index 8271250e..16321347 100644 --- a/gen/openapiv2/headscale/v1/node.swagger.json +++ b/gen/openapiv2/headscale/v1/node.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/policy.swagger.json b/gen/openapiv2/headscale/v1/policy.swagger.json index 63afc575..63057ed0 100644 --- a/gen/openapiv2/headscale/v1/policy.swagger.json +++ b/gen/openapiv2/headscale/v1/policy.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/preauthkey.swagger.json b/gen/openapiv2/headscale/v1/preauthkey.swagger.json index ef16319c..17a2be1a 100644 --- a/gen/openapiv2/headscale/v1/preauthkey.swagger.json +++ b/gen/openapiv2/headscale/v1/preauthkey.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/routes.swagger.json b/gen/openapiv2/headscale/v1/routes.swagger.json index 34eda676..11087f2a 100644 --- a/gen/openapiv2/headscale/v1/routes.swagger.json +++ b/gen/openapiv2/headscale/v1/routes.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/gen/openapiv2/headscale/v1/user.swagger.json b/gen/openapiv2/headscale/v1/user.swagger.json index 1355a9cc..008ca3e8 100644 --- a/gen/openapiv2/headscale/v1/user.swagger.json +++ b/gen/openapiv2/headscale/v1/user.swagger.json @@ -34,6 +34,7 @@ "details": { "type": "array", "items": { + "type": "object", "$ref": "#/definitions/protobufAny" } } diff --git a/go.mod b/go.mod index a0797844..18089bbd 100644 --- a/go.mod +++ b/go.mod @@ -4,30 +4,30 @@ go 1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 - github.com/coreos/go-oidc/v3 v3.10.0 + github.com/coreos/go-oidc/v3 v3.11.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.6.0 github.com/glebarez/sqlite v1.11.0 github.com/go-gormigrate/gormigrate/v2 v2.1.2 - github.com/gofrs/uuid/v5 v5.2.0 + github.com/gofrs/uuid/v5 v5.3.0 github.com/google/go-cmp v0.6.0 github.com/gorilla/mux v1.8.1 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 github.com/jagottsicher/termcolor v1.0.2 - github.com/klauspost/compress v1.17.8 + github.com/klauspost/compress v1.17.9 github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 - github.com/ory/dockertest/v3 v3.10.0 + github.com/ory/dockertest/v3 v3.11.0 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/philip-bui/grpc-zerolog v1.0.1 github.com/pkg/profile v1.7.0 - github.com/prometheus/client_golang v1.19.1 - github.com/prometheus/common v0.48.0 + github.com/prometheus/client_golang v1.20.2 + github.com/prometheus/common v0.58.0 github.com/pterm/pterm v0.12.79 - github.com/puzpuzpuz/xsync/v3 v3.1.0 - github.com/rs/zerolog v1.32.0 - github.com/samber/lo v1.39.0 - github.com/sasha-s/go-deadlock v0.3.1 + github.com/puzpuzpuz/xsync/v3 v3.4.0 + github.com/rs/zerolog v1.33.0 + github.com/samber/lo v1.47.0 + github.com/sasha-s/go-deadlock v0.3.5 github.com/spf13/cobra v1.8.1 github.com/spf13/viper v1.20.0-alpha.6 github.com/stretchr/testify v1.9.0 @@ -35,18 +35,18 @@ require ( github.com/tailscale/tailsql v0.0.0-20240418235827-820559f382c1 github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e go4.org/netipx v0.0.0-20231129151722-fdeea329fbba - golang.org/x/crypto v0.25.0 - golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 - golang.org/x/net v0.27.0 - golang.org/x/oauth2 v0.20.0 - golang.org/x/sync v0.7.0 - google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 - google.golang.org/grpc v1.64.0 - google.golang.org/protobuf v1.34.1 + golang.org/x/crypto v0.26.0 + golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 + golang.org/x/net v0.28.0 + golang.org/x/oauth2 v0.22.0 + golang.org/x/sync v0.8.0 + google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 + google.golang.org/grpc v1.66.0 + google.golang.org/protobuf v1.34.2 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c gopkg.in/yaml.v3 v3.0.1 - gorm.io/driver/postgres v1.5.7 - gorm.io/gorm v1.25.10 + gorm.io/driver/postgres v1.5.9 + gorm.io/gorm v1.25.11 tailscale.com v1.72.1 ) @@ -54,7 +54,7 @@ require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - dario.cat/mergo v1.0.0 // indirect + dario.cat/mergo v1.0.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect @@ -78,7 +78,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coder/websocket v1.8.12 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect @@ -86,21 +86,21 @@ require ( github.com/creachadair/mds v0.14.5 // indirect github.com/dblohm7/wingoes v0.0.0-20240123200102-b75a8a7d7eb0 // indirect github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e // indirect - github.com/docker/cli v26.1.3+incompatible // indirect - github.com/docker/docker v26.1.4+incompatible // indirect + github.com/docker/cli v27.2.0+incompatible // indirect + github.com/docker/docker v27.2.0+incompatible // indirect github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect - github.com/felixge/fgprof v0.9.4 // indirect + github.com/felixge/fgprof v0.9.5 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect github.com/gaissmai/bart v0.11.1 // indirect github.com/glebarez/go-sqlite v1.22.0 // indirect github.com/go-jose/go-jose/v3 v3.0.3 // indirect - github.com/go-jose/go-jose/v4 v4.0.1 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/go-viper/mapstructure/v2 v2.0.0 // indirect + github.com/go-viper/mapstructure/v2 v2.1.0 // indirect github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect @@ -110,20 +110,20 @@ require ( github.com/google/go-github v17.0.0+incompatible // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect - github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 // indirect + github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gookit/color v1.5.4 // indirect github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect - github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/illarion/gonotify v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect - github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -134,11 +134,10 @@ require ( github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect - github.com/lib/pq v1.10.7 // indirect github.com/lithammer/fuzzysearch v1.1.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/sdnotify v1.0.0 // indirect @@ -146,21 +145,21 @@ require ( github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/miekg/dns v1.1.58 // indirect github.com/mitchellh/go-ps v1.0.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/term v0.5.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect - github.com/opencontainers/runc v1.1.12 // indirect - github.com/pelletier/go-toml/v2 v2.2.2 // indirect - github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/opencontainers/runc v1.1.14 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect @@ -169,7 +168,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect - github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect @@ -193,19 +192,19 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.uber.org/multierr v1.11.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect - golang.org/x/mod v0.19.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect + golang.org/x/text v0.17.0 // indirect golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.23.0 // indirect + golang.org/x/tools v0.24.0 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wireguard/windows v0.5.3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect - modernc.org/libc v1.50.6 // indirect + modernc.org/libc v1.60.1 // indirect modernc.org/mathutil v1.6.0 // indirect modernc.org/memory v1.8.0 // indirect - modernc.org/sqlite v1.29.9 // indirect + modernc.org/sqlite v1.32.0 // indirect ) diff --git a/go.sum b/go.sum index fb5b93c0..2213f423 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtE atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/mkcert v1.4.4 h1:8eVbbwfVlaqUM7OwuftKc2nuYOoTDQWqsoXmzoXZdbc= @@ -88,8 +88,8 @@ github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6 github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= @@ -112,8 +112,8 @@ github.com/containerd/continuity v0.4.3 h1:6HVkalIp+2u1ZLH1J/pYX2oBVXlJZvh1X1A7b github.com/containerd/continuity v0.4.3/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= -github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU= -github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac= +github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= +github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creachadair/mds v0.14.5 h1:2amuO4yCbQkaAyDoLO5iCbwbTRQZz4EpRhOejQbf4+8= @@ -134,10 +134,10 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= -github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= -github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= -github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v27.2.0+incompatible h1:yHD1QEB1/0vr5eBNpu8tncu8gWxg8EydFPOSKHzXSMM= +github.com/docker/cli v27.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -151,8 +151,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= -github.com/felixge/fgprof v0.9.4 h1:ocDNwMFlnA0NU0zSB3I52xkO4sFXk80VK9lXjLClu88= -github.com/felixge/fgprof v0.9.4/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= +github.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY= +github.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -171,27 +171,27 @@ github.com/go-gormigrate/gormigrate/v2 v2.1.2 h1:F/d1hpHbRAvKezziV2CC5KUE82cVe9z github.com/go-gormigrate/gormigrate/v2 v2.1.2/go.mod h1:9nHVX6z3FCMCQPA7PThGcA55t22yKQfK/Dnsf5i7hUo= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= -github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= -github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= -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.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= +github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= -github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM= -github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= +github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= +github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= @@ -222,8 +222,8 @@ github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdF github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= -github.com/google/pprof v0.0.0-20240509144519-723abb6459b7 h1:velgFPYr1X9TDwLIfkV7fWqsFlf7TeP11M/7kPd/dVI= -github.com/google/pprof v0.0.0-20240509144519-723abb6459b7/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25 h1:sEDPKUw6iPjczdu33njxFjO6tYa9bfc0z/QyB/zSsBw= +github.com/google/pprof v0.0.0-20240829160300-da1f7e9f2b25/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -240,10 +240,10 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 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/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU= @@ -260,10 +260,10 @@ github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914 h1:kD8PseueGeYii github.com/insomniacslk/dhcp v0.0.0-20240129002554-15c9b8791914/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA= -github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= -github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jagottsicher/termcolor v1.0.2 h1:fo0c51pQSuLBN1+yVX2ZE+hE+P7ULb/TY8eRowJnrsM= @@ -288,13 +288,13 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNU github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a h1:+RR6SqnTkDLWyICxS1xpjCi/3dhyV+TgZwA6Ww3KncQ= github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a/go.mod h1:YTtCCM3ryyfiu4F7t8HQ1mxvp1UBdWM2r6Xa+nGWvDk= @@ -308,9 +308,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -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.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -323,8 +325,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= -github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -340,12 +342,12 @@ github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= @@ -356,18 +358,18 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= +github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= -github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/ory/dockertest/v3 v3.11.0 h1:OiHcxKAvSDUwsEVh2BjxQQc/5EHz9n0va9awCtNGuyA= +github.com/ory/dockertest/v3 v3.11.0/go.mod h1:VIPxS1gwT9NpPOrfD3rACs8Y9Z7yhzO4SB194iUDnUI= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= -github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= -github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7 h1:Dx7Ovyv/SFnMFw3fD4oEoeorXc6saIiQ23LrGLth0Gw= +github.com/petermattis/goid v0.0.0-20240813172612-4fcff4a6cae7/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/philip-bui/grpc-zerolog v1.0.1 h1:EMacvLRUd2O1K0eWod27ZP5CY1iTNkhBDLSN+Q4JEvA= github.com/philip-bui/grpc-zerolog v1.0.1/go.mod h1:qXbiq/2X4ZUMMshsqlWyTHOcw7ns+GZmlqZZN05ZHcQ= github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -386,15 +388,15 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= -github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= -github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= +github.com/prometheus/client_golang v1.20.2 h1:5ctymQzZlyOON1666svgwn3s6IKWgfbjsejTMiXIyjg= +github.com/prometheus/client_golang v1.20.2/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.58.0 h1:N+N8vY4/23r6iYfD3UQZUoJPnUYAo7v6LG5XZxjZTXo= +github.com/prometheus/common v0.58.0/go.mod h1:GpWM7dewqmVYcd7SmRaiWVe9SSqjf0UrwnYnpEZNuT0= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI= github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg= github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE= @@ -404,8 +406,8 @@ github.com/pterm/pterm v0.12.36/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5b github.com/pterm/pterm v0.12.40/go.mod h1:ffwPLwlbXxP+rxT0GsgDTzS3y3rmpAO1NMjUkGTYf8s= github.com/pterm/pterm v0.12.79 h1:lH3yrYMhdpeqX9y5Ep1u7DejyHy7NSQg9qrBjF9dFT4= github.com/pterm/pterm v0.12.79/go.mod h1:1v/gzOF1N0FsjbgTHZ1wVycRkKiatFvJSJC4IGaQAAo= -github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4= -github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= +github.com/puzpuzpuz/xsync/v3 v3.4.0 h1:DuVBAdXuGFHv8adVXjWWZ63pJq+NRXOWVXlKDBZ+mJ4= +github.com/puzpuzpuz/xsync/v3 v3.4.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -415,17 +417,17 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= -github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= +github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk= github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0= -github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA= -github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= -github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= -github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/sasha-s/go-deadlock v0.3.5 h1:tNCOEEDG6tBqrNDOX35j/7hL5FcFViG6awUGROb2NsU= +github.com/sasha-s/go-deadlock v0.3.5/go.mod h1:bugP6EGbdGYObIlx7pUZtWqlvo8k9H6vCBBsiChJQ5U= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -436,8 +438,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -447,7 +449,6 @@ github.com/spf13/viper v1.20.0-alpha.6/go.mod h1:CGBZzv0c9fOUASm6rfus4wdeIjR/04N github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -457,7 +458,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -537,11 +537,11 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= -golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA= +golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= @@ -554,8 +554,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= -golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -568,11 +568,11 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= -golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= -golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA= +golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -581,8 +581,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -614,8 +614,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -623,8 +623,8 @@ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuX golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -632,8 +632,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -647,8 +647,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= -golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -662,19 +662,19 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291 h1:4HZJ3Xv1cmrJ+0aFo304Zn79ur1HMxptAE7aCPNLSqc= -google.golang.org/genproto/googleapis/api v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:RGnPtTG7r4i8sPlNyDeikXF99hMM+hN6QMm4ooG9g2g= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291 h1:AgADTJarZTBqgjiUzRgfaBchgYB3/WFTC80GPwsMcRI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240515191416-fc5f0ca64291/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= -google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= -google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= -google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/grpc v1.66.0 h1:DibZuoBznOxbDQxRINckZcUvnCEvrW9pcWIE2yF9r1c= +google.golang.org/grpc v1.66.0/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -689,12 +689,12 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= -gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= -gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s= -gorm.io/gorm v1.25.10/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8= +gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI= +gorm.io/gorm v1.25.11 h1:/Wfyg1B/je1hnDx3sMkX+gAlxrlZpn6X0BXRlwXlvHg= +gorm.io/gorm v1.25.11/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -703,18 +703,18 @@ honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -modernc.org/cc/v4 v4.21.2 h1:dycHFB/jDc3IyacKipCNSDrjIC0Lm1hyoWOZTRR20Lk= -modernc.org/cc/v4 v4.21.2/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v4 v4.17.7 h1:+MG+Np7uYtsuPvtoH3KtZ1+pqNiJAOqqqVIxggE1iIo= -modernc.org/ccgo/v4 v4.17.7/go.mod h1:x87xuLLXuJv3Nn5ULTUqJn/HsTMMMiT1Eavo6rz1NiY= +modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= +modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= +modernc.org/ccgo/v4 v4.21.0 h1:kKPI3dF7RIag8YcToh5ZwDcVMIv6VGa0ED5cvh0LMW4= +modernc.org/ccgo/v4 v4.21.0/go.mod h1:h6kt6H/A2+ew/3MW/p6KEoQmrq/i3pr0J/SiwiaF/g0= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= -modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= -modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= +modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M= +modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= -modernc.org/libc v1.50.6 h1:72NPEFMyKP01RJrKXS2eLXv35UklKqlJZ1b9P7gSo6I= -modernc.org/libc v1.50.6/go.mod h1:8lr2m1THY5Z3ikGyUc3JhLEQg1oaIBz/AQixw8/eksQ= +modernc.org/libc v1.60.1 h1:at373l8IFRTkJIkAU85BIuUoBM4T1b51ds0E1ovPG2s= +modernc.org/libc v1.60.1/go.mod h1:xJuobKuNxKH3RUatS7GjR+suWj+5c2K7bi4m/S5arOY= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= @@ -723,8 +723,8 @@ modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow= -modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= +modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s= +modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= diff --git a/proto/headscale/v1/headscale.proto b/proto/headscale/v1/headscale.proto index 183927ed..7324b65a 100644 --- a/proto/headscale/v1/headscale.proto +++ b/proto/headscale/v1/headscale.proto @@ -209,7 +209,6 @@ service HeadscaleService { } // --- Policy end --- - // Implement Tailscale API // rpc GetDevice(GetDeviceRequest) returns(GetDeviceResponse) { // option(google.api.http) = { diff --git a/proto/headscale/v1/policy.proto b/proto/headscale/v1/policy.proto index 3c929385..995f3af8 100644 --- a/proto/headscale/v1/policy.proto +++ b/proto/headscale/v1/policy.proto @@ -5,17 +5,17 @@ option go_package = "github.com/juanfont/headscale/gen/go/v1"; import "google/protobuf/timestamp.proto"; message SetPolicyRequest { - string policy = 1; + string policy = 1; } message SetPolicyResponse { - string policy = 1; - google.protobuf.Timestamp updated_at = 2; + string policy = 1; + google.protobuf.Timestamp updated_at = 2; } message GetPolicyRequest {} message GetPolicyResponse { - string policy = 1; - google.protobuf.Timestamp updated_at = 2; + string policy = 1; + google.protobuf.Timestamp updated_at = 2; } \ No newline at end of file From e43d6a0361c2f0567f2ae79852ca86db13ddc6bb Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 4 Sep 2024 14:38:38 +0200 Subject: [PATCH 28/60] Move flags after the command (#2100) The built-in help also shows flags to given after the command. Align documentation examples accordingly. --- docs/running-headscale-container.md | 4 ++-- docs/running-headscale-linux-manual.md | 4 ++-- docs/running-headscale-linux.md | 4 ++-- docs/running-headscale-openbsd.md | 4 ++-- docs/running-headscale-sealos.md | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index ef622f4e..087dae30 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -117,7 +117,7 @@ To register a machine when running `headscale` in a container, take the headscal ```shell docker exec headscale \ - headscale --user myfirstuser nodes register --key + headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -126,7 +126,7 @@ Generate a key using the command line: ```shell docker exec headscale \ - headscale --user myfirstuser preauthkeys create --reusable --expiration 24h + headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 25d47638..720390d8 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -92,7 +92,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -100,7 +100,7 @@ headscale --user myfirstuser nodes register --key Generate a key using the command line: ```shell -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index f08789c4..4be2e693 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -78,7 +78,7 @@ tailscale up --login-server Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -86,7 +86,7 @@ headscale --user myfirstuser nodes register --key Generate a key using the command line: ```shell -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that is used to diff --git a/docs/running-headscale-openbsd.md b/docs/running-headscale-openbsd.md index f3e0548e..449034ba 100644 --- a/docs/running-headscale-openbsd.md +++ b/docs/running-headscale-openbsd.md @@ -129,7 +129,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL Register the machine: ```shell -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -137,7 +137,7 @@ headscale --user myfirstuser nodes register --key Generate a key using the command line: ```shell -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: diff --git a/docs/running-headscale-sealos.md b/docs/running-headscale-sealos.md index 1e3fe3ac..52f5c7ec 100644 --- a/docs/running-headscale-sealos.md +++ b/docs/running-headscale-sealos.md @@ -41,7 +41,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL To register a machine when running headscale in [Sealos](https://sealos.io), click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then take the headscale command: ```bash -headscale --user myfirstuser nodes register --key +headscale nodes register --user myfirstuser --key ``` ### Register machine using a pre authenticated key @@ -49,7 +49,7 @@ headscale --user myfirstuser nodes register --key click on 'Terminal' button on the right side of the headscale application's detail page to access the Terminal of the headscale application, then generate a key using the command line: ```bash -headscale --user myfirstuser preauthkeys create --reusable --expiration 24h +headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` This will return a pre-authenticated key that can be used to connect a node to `headscale` during the `tailscale` command: From 35bfe7ced04079af7017c044140451c5f6622af5 Mon Sep 17 00:00:00 2001 From: nblock Date: Thu, 5 Sep 2024 12:08:50 +0200 Subject: [PATCH 29/60] Add support for service reload and sync service file (#2102) * Add support for service reload and sync service file * Copy the systemd.service file to the manual linux docs and adjust the path to the headscale binary to match with the previous documentation blocks. Unfortunately, there seems to be no easy way to include a file in mkdocs. * Remove a redundant "deprecation" block. The beginning of the documentation already states that. * Add `ExecReload` to the systemd.service file. Fixes: #2016 * Its called systemd * Fix link to systemd homepage --- docs/packaging/headscale.systemd.service | 1 + docs/running-headscale-linux-manual.md | 44 +++++------------------- docs/running-headscale-linux.md | 2 +- integration/dns_test.go | 2 +- 4 files changed, 11 insertions(+), 38 deletions(-) diff --git a/docs/packaging/headscale.systemd.service b/docs/packaging/headscale.systemd.service index 14e31618..37d5f5d3 100644 --- a/docs/packaging/headscale.systemd.service +++ b/docs/packaging/headscale.systemd.service @@ -9,6 +9,7 @@ Type=simple User=headscale Group=headscale ExecStart=/usr/bin/headscale serve +ExecReload=/usr/bin/kill -HUP $MAINPID Restart=always RestartSec=5 diff --git a/docs/running-headscale-linux-manual.md b/docs/running-headscale-linux-manual.md index 720390d8..3a0d91e0 100644 --- a/docs/running-headscale-linux-manual.md +++ b/docs/running-headscale-linux-manual.md @@ -8,7 +8,7 @@ ## Goal This documentation has the goal of showing a user how-to set up and run `headscale` on Linux. -In additional to the "get up and running section", there is an optional [SystemD section](#running-headscale-in-the-background-with-systemd) +In additional to the "get up and running section", there is an optional [systemd section](#running-headscale-in-the-background-with-systemd) describing how to make `headscale` run properly in a server environment. ## Configure and run `headscale` @@ -66,7 +66,7 @@ describing how to make `headscale` run properly in a server environment. To continue the tutorial, open a new terminal and let it run in the background. Alternatively use terminal emulators like [tmux](https://github.com/tmux/tmux) or [screen](https://www.gnu.org/software/screen/). - To run `headscale` in the background, please follow the steps in the [SystemD section](#running-headscale-in-the-background-with-systemd) before continuing. + To run `headscale` in the background, please follow the steps in the [systemd section](#running-headscale-in-the-background-with-systemd) before continuing. 1. Verify `headscale` is running: Verify `headscale` is available: @@ -109,42 +109,14 @@ This will return a pre-authenticated key that can be used to connect a node to ` tailscale up --login-server --authkey ``` -## Running `headscale` in the background with SystemD +## Running `headscale` in the background with systemd -:warning: **Deprecated**: This part is very outdated and you should use the [pre-packaged Headscale for this](./running-headscale-linux.md) - -This section demonstrates how to run `headscale` as a service in the background with [SystemD](https://www.freedesktop.org/wiki/Software/systemd/). +This section demonstrates how to run `headscale` as a service in the background with [systemd](https://systemd.io/). This should work on most modern Linux distributions. -1. Create a SystemD service configuration at `/etc/systemd/system/headscale.service` containing: - - ```systemd - [Unit] - Description=headscale controller - After=syslog.target - After=network.target - - [Service] - Type=simple - User=headscale - Group=headscale - ExecStart=/usr/local/bin/headscale serve - Restart=always - RestartSec=5 - - # Optional security enhancements - NoNewPrivileges=yes - PrivateTmp=yes - ProtectSystem=strict - ProtectHome=yes - WorkingDirectory=/var/lib/headscale - ReadWritePaths=/var/lib/headscale /var/run/headscale - AmbientCapabilities=CAP_NET_BIND_SERVICE - RuntimeDirectory=headscale - - [Install] - WantedBy=multi-user.target - ``` +1. Copy [headscale's systemd service file](./packaging/headscale.systemd.service) to + `/etc/systemd/system/headscale.service` and adjust it to suit your local setup. The following parameters likely need + to be modified: `ExecStart`, `WorkingDirectory`, `ReadWritePaths`. Note that when running as the headscale user ensure that, either you add your current user to the headscale group: @@ -164,7 +136,7 @@ This should work on most modern Linux distributions. unix_socket: /var/run/headscale/headscale.sock ``` -1. Reload SystemD to load the new configuration file: +1. Reload systemd to load the new configuration file: ```shell systemctl daemon-reload diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 4be2e693..ffa510a6 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -8,7 +8,7 @@ Get Headscale up and running. -This includes running Headscale with SystemD. +This includes running Headscale with systemd. ## Migrating from manual install diff --git a/integration/dns_test.go b/integration/dns_test.go index 60f05199..f7973300 100644 --- a/integration/dns_test.go +++ b/integration/dns_test.go @@ -86,7 +86,7 @@ func TestResolveMagicDNS(t *testing.T) { // 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). +// if available (like systemd-resolved). func TestValidateResolvConf(t *testing.T) { IntegrationSkip(t) From 6609f60938ad5410f4229db339c7c65394f36293 Mon Sep 17 00:00:00 2001 From: greizgh Date: Thu, 5 Sep 2024 13:37:05 +0200 Subject: [PATCH 30/60] actually lint file on CI (#2018) * replace deprecated golangci-lint output format CI was producing this kind of messages: > [config_reader] The output format `github-actions` is deprecated, please use `colored-line-number` * Actually lint files on CI --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 8f38f9d7..94953fbc 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,7 +31,7 @@ jobs: - name: golangci-lint if: steps.changed-files.outputs.files == 'true' - run: nix develop --command -- golangci-lint run --new-from-rev=${{github.event.pull_request.base.sha}} --out-format=github-actions . + run: nix develop --command -- golangci-lint run --new-from-rev=${{github.event.pull_request.base.sha}} --out-format=colored-line-number prettier-lint: runs-on: ubuntu-latest From 1c34101e72683515bfb7ed44fc6999f3211cddee Mon Sep 17 00:00:00 2001 From: nblock Date: Thu, 5 Sep 2024 13:50:10 +0200 Subject: [PATCH 31/60] Remove outdated proposals (#2104) Fixes: #2101 --- docs/proposals/001-acls.md | 362 --------------------------- docs/proposals/002-better-routing.md | 48 ---- mkdocs.yml | 3 - 3 files changed, 413 deletions(-) delete mode 100644 docs/proposals/001-acls.md delete mode 100644 docs/proposals/002-better-routing.md diff --git a/docs/proposals/001-acls.md b/docs/proposals/001-acls.md deleted file mode 100644 index 74bcd13e..00000000 --- a/docs/proposals/001-acls.md +++ /dev/null @@ -1,362 +0,0 @@ -# ACLs - -A key component of tailscale is the notion of Tailnet. This notion is hidden -but the implications that it have on how to use tailscale are not. - -For tailscale an [tailnet](https://tailscale.com/kb/1136/tailnet/) is the -following: - -> For personal users, you are a tailnet of many devices and one person. Each -> device gets a private Tailscale IP address in the CGNAT range and every -> device can talk directly to every other device, wherever they are on the -> internet. -> -> For businesses and organizations, a tailnet is many devices and many users. -> It can be based on your Microsoft Active Directory, your Google Workspace, a -> GitHub organization, Okta tenancy, or other identity provider namespace. All -> of the devices and users in your tailnet can be seen by the tailnet -> administrators in the Tailscale admin console. There you can apply -> tailnet-wide configuration, such as ACLs that affect visibility of devices -> inside your tailnet, DNS settings, and more. - -## Current implementation and issues - -Currently in headscale, the namespaces are used both as tailnet and users. The -issue is that if we want to use the ACL's we can't use both at the same time. - -Tailnet's cannot communicate with each others. So we can't have an ACL that -authorize tailnet (namespace) A to talk to tailnet (namespace) B. - -We also can't write ACLs based on the users (namespaces in headscale) since all -devices belong to the same user. - -With the current implementation the only ACL that we can user is to associate -each headscale IP to a host manually then write the ACLs according to this -manual mapping. - -```json -{ - "hosts": { - "host1": "100.64.0.1", - "server": "100.64.0.2" - }, - "acls": [ - { "action": "accept", "users": ["host1"], "ports": ["host2:80,443"] } - ] -} -``` - -While this works, it requires a lot of manual editing on the configuration and -to keep track of all devices IP address. - -## Proposition for a next implementation - -In order to ease the use of ACL's we need to split the tailnet and users -notion. - -A solution could be to consider a headscale server (in it's entirety) as a -tailnet. - -For personal users the default behavior could either allow all communications -between all namespaces (like tailscale) or disallow all communications between -namespaces (current behavior). - -For businesses and organisations, viewing a headscale instance a single tailnet -would allow users (namespace) to talk to each other with the ACLs. As described -in tailscale's documentation [[1]], a server should be tagged and personal -devices should be tied to a user. Translated in headscale's terms each user can -have multiple devices and all those devices should be in the same namespace. -The servers should be tagged and used as such. - -This implementation would render useless the sharing feature that is currently -implemented since an ACL could do the same. Simplifying to only one user -interface to do one thing is easier and less confusing for the users. - -To better suit the ACLs in this proposition, it's advised to consider that each -namespaces belong to one person. This person can have multiple devices, they -will all be considered as the same user in the ACLs. OIDC feature wouldn't need -to map people to namespace, just create a namespace if the person isn't -registered yet. - -As a sidenote, users would like to write ACLs as YAML. We should offer users -the ability to rules in either format (HuJSON or YAML). - -[1]: https://tailscale.com/kb/1068/acl-tags/ - -## Example - -Let's build an example use case for a small business (It may be the place where -ACL's are the most useful). - -We have a small company with a boss, an admin, two developer and an intern. - -The boss should have access to all servers but not to the users hosts. Admin -should also have access to all hosts except that their permissions should be -limited to maintaining the hosts (for example purposes). The developers can do -anything they want on dev hosts, but only watch on productions hosts. Intern -can only interact with the development servers. - -Each user have at least a device connected to the network and we have some -servers. - -- database.prod -- database.dev -- app-server1.prod -- app-server1.dev -- billing.internal - -### Current headscale implementation - -Let's create some namespaces - -```bash -headscale namespaces create prod -headscale namespaces create dev -headscale namespaces create internal -headscale namespaces create users - -headscale nodes register -n users boss-computer -headscale nodes register -n users admin1-computer -headscale nodes register -n users dev1-computer -headscale nodes register -n users dev1-phone -headscale nodes register -n users dev2-computer -headscale nodes register -n users intern1-computer - -headscale nodes register -n prod database -headscale nodes register -n prod app-server1 - -headscale nodes register -n dev database -headscale nodes register -n dev app-server1 - -headscale nodes register -n internal billing - -headscale nodes list -ID | Name | Namespace | IP address -1 | boss-computer | users | 100.64.0.1 -2 | admin1-computer | users | 100.64.0.2 -3 | dev1-computer | users | 100.64.0.3 -4 | dev1-phone | users | 100.64.0.4 -5 | dev2-computer | users | 100.64.0.5 -6 | intern1-computer | users | 100.64.0.6 -7 | database | prod | 100.64.0.7 -8 | app-server1 | prod | 100.64.0.8 -9 | database | dev | 100.64.0.9 -10 | app-server1 | dev | 100.64.0.10 -11 | internal | internal | 100.64.0.11 -``` - -In order to only allow the communications related to our description above we -need to add the following ACLs - -```json -{ - "hosts": { - "boss-computer": "100.64.0.1", - "admin1-computer": "100.64.0.2", - "dev1-computer": "100.64.0.3", - "dev1-phone": "100.64.0.4", - "dev2-computer": "100.64.0.5", - "intern1-computer": "100.64.0.6", - "prod-app-server1": "100.64.0.8" - }, - "groups": { - "group:dev": ["dev1-computer", "dev1-phone", "dev2-computer"], - "group:admin": ["admin1-computer"], - "group:boss": ["boss-computer"], - "group:intern": ["intern1-computer"] - }, - "acls": [ - // boss have access to all servers but no users hosts - { - "action": "accept", - "users": ["group:boss"], - "ports": ["prod:*", "dev:*", "internal:*"] - }, - - // admin have access to administration port (lets only consider port 22 here) - { - "action": "accept", - "users": ["group:admin"], - "ports": ["prod:22", "dev:22", "internal:22"] - }, - - // dev can do anything on dev servers and check access on prod servers - { - "action": "accept", - "users": ["group:dev"], - "ports": ["dev:*", "prod-app-server1:80,443"] - }, - - // interns only have access to port 80 and 443 on dev servers (lame internship) - { "action": "accept", "users": ["group:intern"], "ports": ["dev:80,443"] }, - - // users can access their own devices - { - "action": "accept", - "users": ["dev1-computer"], - "ports": ["dev1-phone:*"] - }, - { - "action": "accept", - "users": ["dev1-phone"], - "ports": ["dev1-computer:*"] - }, - - // internal namespace communications should still be allowed within the namespace - { "action": "accept", "users": ["dev"], "ports": ["dev:*"] }, - { "action": "accept", "users": ["prod"], "ports": ["prod:*"] }, - { "action": "accept", "users": ["internal"], "ports": ["internal:*"] } - ] -} -``` - -Since communications between namespace isn't possible we also have to share the -devices between the namespaces. - -```bash - -// add boss host to prod, dev and internal network -headscale nodes share -i 1 -n prod -headscale nodes share -i 1 -n dev -headscale nodes share -i 1 -n internal - -// add admin computer to prod, dev and internal network -headscale nodes share -i 2 -n prod -headscale nodes share -i 2 -n dev -headscale nodes share -i 2 -n internal - -// add all dev to prod and dev network -headscale nodes share -i 3 -n dev -headscale nodes share -i 4 -n dev -headscale nodes share -i 3 -n prod -headscale nodes share -i 4 -n prod -headscale nodes share -i 5 -n dev -headscale nodes share -i 5 -n prod - -headscale nodes share -i 6 -n dev -``` - -This fake network have not been tested but it should work. Operating it could -be quite tedious if the company grows. Each time a new user join we have to add -it to a group, and share it to the correct namespaces. If the user want -multiple devices we have to allow communication to each of them one by one. If -business conduct a change in the organisations we may have to rewrite all acls -and reorganise all namespaces. - -If we add servers in production we should also update the ACLs to allow dev -access to certain category of them (only app servers for example). - -### example based on the proposition in this document - -Let's create the namespaces - -```bash -headscale namespaces create boss -headscale namespaces create admin1 -headscale namespaces create dev1 -headscale namespaces create dev2 -headscale namespaces create intern1 -``` - -We don't need to create namespaces for the servers because the servers will be -tagged. When registering the servers we will need to add the flag -`--advertised-tags=tag:,tag:`, and the user (namespace) that is -registering the server should be allowed to do it. Since anyone can add tags to -a server they can register, the check of the tags is done on headscale server -and only valid tags are applied. A tag is valid if the namespace that is -registering it is allowed to do it. - -Here are the ACL's to implement the same permissions as above: - -```json -{ - // groups are simpler and only list the namespaces name - "groups": { - "group:boss": ["boss"], - "group:dev": ["dev1", "dev2"], - "group:admin": ["admin1"], - "group:intern": ["intern1"] - }, - "tagOwners": { - // the administrators can add servers in production - "tag:prod-databases": ["group:admin"], - "tag:prod-app-servers": ["group:admin"], - - // the boss can tag any server as internal - "tag:internal": ["group:boss"], - - // dev can add servers for dev purposes as well as admins - "tag:dev-databases": ["group:admin", "group:dev"], - "tag:dev-app-servers": ["group:admin", "group:dev"] - - // interns cannot add servers - }, - "acls": [ - // boss have access to all servers - { - "action": "accept", - "users": ["group:boss"], - "ports": [ - "tag:prod-databases:*", - "tag:prod-app-servers:*", - "tag:internal:*", - "tag:dev-databases:*", - "tag:dev-app-servers:*" - ] - }, - - // admin have only access to administrative ports of the servers - { - "action": "accept", - "users": ["group:admin"], - "ports": [ - "tag:prod-databases:22", - "tag:prod-app-servers:22", - "tag:internal:22", - "tag:dev-databases:22", - "tag:dev-app-servers:22" - ] - }, - - { - "action": "accept", - "users": ["group:dev"], - "ports": [ - "tag:dev-databases:*", - "tag:dev-app-servers:*", - "tag:prod-app-servers:80,443" - ] - }, - - // servers should be able to talk to database. Database should not be able to initiate connections to server - { - "action": "accept", - "users": ["tag:dev-app-servers"], - "ports": ["tag:dev-databases:5432"] - }, - { - "action": "accept", - "users": ["tag:prod-app-servers"], - "ports": ["tag:prod-databases:5432"] - }, - - // interns have access to dev-app-servers only in reading mode - { - "action": "accept", - "users": ["group:intern"], - "ports": ["tag:dev-app-servers:80,443"] - }, - - // we still have to allow internal namespaces communications since nothing guarantees that each user have their own namespaces. This could be talked over. - { "action": "accept", "users": ["boss"], "ports": ["boss:*"] }, - { "action": "accept", "users": ["dev1"], "ports": ["dev1:*"] }, - { "action": "accept", "users": ["dev2"], "ports": ["dev2:*"] }, - { "action": "accept", "users": ["admin1"], "ports": ["admin1:*"] }, - { "action": "accept", "users": ["intern1"], "ports": ["intern1:*"] } - ] -} -``` - -With this implementation, the sharing step is not necessary. Maintenance cost -of the ACL file is lower and less tedious (no need to map hostname and IP's -into it). diff --git a/docs/proposals/002-better-routing.md b/docs/proposals/002-better-routing.md deleted file mode 100644 index c56a38ff..00000000 --- a/docs/proposals/002-better-routing.md +++ /dev/null @@ -1,48 +0,0 @@ -# Better route management - -As of today, route management in Headscale is very basic and does not allow for much flexibility, including implementing subnet HA, 4via6 or more advanced features. We also have a number of bugs (e.g., routes exposed by ephemeral nodes) - -This proposal aims to improve the route management. - -## Current situation - -Routes advertised by the nodes are read from the Hostinfo struct. If approved from the the CLI or via autoApprovers, the route is added to the EnabledRoutes field in `Machine`. - -This means that the advertised routes are not persisted in the database, as Hostinfo is always replaced. In the same way, EnabledRoutes can get out of sync with the actual routes in the node. - -In case of colliding routes (i.e., subnets that are exposed from multiple nodes), we are currently just sending all of them in `PrimaryRoutes`... and hope for the best. (`PrimaryRoutes` is the field in `Node` used for subnet failover). - -## Proposal - -The core part is to create a new `Route` struct (and DB table), with the following fields: - -```go -type Route struct { - ID uint64 `gorm:"primary_key"` - - Machine *Machine - Prefix IPPrefix - - Advertised bool - Enabled bool - IsPrimary bool - - - CreatedAt *time.Time - UpdatedAt *time.Time - DeletedAt *time.Time -} -``` - -- The `Advertised` field is set to true if the route is being advertised by the node. It is set to false if the route is removed. This way we can indicate if a later enabled route has stopped being advertised. A similar behaviour happens in the Tailscale.com control panel. - -- The `Enabled` field is set to true if the route is enabled - via CLI or autoApprovers. - -- `IsPrimary` indicates if Headscale has selected this route as the primary route for that particular subnet. This allows us to implement subnet failover. This would be fully automatic if there is more than subnet routers advertising the same network - which is the behaviour of Tailscale.com. - -## Stuff to bear in mind - -- We need to make sure to migrate the current `EnabledRoutes` of `Machine` into the new table. -- When a node stops sharing a subnet, I reckon we should mark it both as not `Advertised` and not `Enabled`. Users should re-enable it if the node advertises it again. -- If only one subnet router is advertising a subnet, we should mark it as primary. -- Regarding subnet failover, the current behaviour of Tailscale.com is to perform the failover after 15 seconds from the node disconnecting from their control panel. I reckon we cannot do the same currently. Our maximum granularity is the keep alive period. diff --git a/mkdocs.yml b/mkdocs.yml index 2dca103d..c14fd716 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -141,6 +141,3 @@ nav: - Android: android-client.md - Windows: windows-client.md - iOS: iOS-client.md - - Proposals: - - ACLs: proposals/001-acls.md - - Better routing: proposals/002-better-routing.md From 42d2c27853ce98cf50df84a4e4c14409c618f15a Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Sep 2024 14:00:19 +0200 Subject: [PATCH 32/60] fix goreleaser warnings (#2106) * add version to goreleaser config Signed-off-by: Kristoffer Dalby * rename deprected setting Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .goreleaser.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.goreleaser.yml b/.goreleaser.yml index 4e91c74d..4aabde4b 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,4 +1,5 @@ --- +version: 2 before: hooks: - go mod tidy -compat=1.22 @@ -184,7 +185,7 @@ kos: checksum: name_template: "checksums.txt" snapshot: - name_template: "{{ .Tag }}-next" + version_template: "{{ .Tag }}-next" changelog: sort: asc filters: From adc084f20f843d7963c999764fa83939668d2d2c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Sep 2024 14:00:36 +0200 Subject: [PATCH 33/60] add no stalebot exception (#2107) Signed-off-by: Kristoffer Dalby --- .github/workflows/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index f7c4ae75..592929cb 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -20,4 +20,5 @@ jobs: close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." days-before-pr-stale: -1 days-before-pr-close: -1 + exempt-issue-labels: "no-stale-bot" repo-token: ${{ secrets.GITHUB_TOKEN }} From f368ed01ed18b1d9388879f17f4e78d29218fbd9 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Thu, 5 Sep 2024 16:46:20 +0200 Subject: [PATCH 34/60] 2068 AutoApprovers tests (#2105) * replace old suite approved routes test with table driven Signed-off-by: Kristoffer Dalby * add test to reproduce issue Signed-off-by: Kristoffer Dalby * add integration test for 2068 Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/db/node_test.go | 152 ++++++++++++++++-------- hscontrol/poll.go | 23 +--- hscontrol/util/net.go | 19 +++ integration/route_test.go | 90 ++++++++++++++ 5 files changed, 215 insertions(+), 70 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index d5b362b7..ed194da1 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -55,6 +55,7 @@ jobs: - TestEnablingRoutes - TestHASubnetRouterFailover - TestEnableDisableAutoApprovedRoute + - TestAutoApprovedSubRoute2068 - TestSubnetRouteACL - TestHeadscale - TestCreateTailscale diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index c83da120..94cce13b 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -6,6 +6,7 @@ import ( "math/big" "net/netip" "regexp" + "sort" "strconv" "sync" "testing" @@ -518,8 +519,37 @@ func TestHeadscale_generateGivenName(t *testing.T) { } } -func (s *Suite) TestAutoApproveRoutes(c *check.C) { - acl := []byte(` +func TestAutoApproveRoutes(t *testing.T) { + tests := []struct { + name string + acl string + routes []netip.Prefix + want []netip.Prefix + }{ + { + name: "2068-approve-issue-sub", + acl: ` +{ + "groups": { + "group:k8s": ["test"] + }, + + "acls": [ + {"action": "accept", "users": ["*"], "ports": ["*:*"]}, + ], + + "autoApprovers": { + "routes": { + "10.42.0.0/16": ["test"], + } + } +}`, + routes: []netip.Prefix{netip.MustParsePrefix("10.42.7.0/24")}, + want: []netip.Prefix{netip.MustParsePrefix("10.42.7.0/24")}, + }, + { + name: "2068-approve-issue-sub", + acl: ` { "tagOwners": { "tag:exit": ["test"], @@ -540,61 +570,83 @@ func (s *Suite) TestAutoApproveRoutes(c *check.C) { "10.11.0.0/16": ["test"], } } -} - `) - - pol, err := policy.LoadACLPolicyFromBytes(acl) - c.Assert(err, check.IsNil) - c.Assert(pol, check.NotNil) - - user, err := db.CreateUser("test") - c.Assert(err, check.IsNil) - - pak, err := db.CreatePreAuthKey(user.Name, false, false, nil, nil) - c.Assert(err, check.IsNil) - - nodeKey := key.NewNode() - machineKey := key.NewMachine() - - defaultRouteV4 := netip.MustParsePrefix("0.0.0.0/0") - defaultRouteV6 := netip.MustParsePrefix("::/0") - route1 := netip.MustParsePrefix("10.10.0.0/16") - // Check if a subprefix of an autoapproved route is approved - route2 := netip.MustParsePrefix("10.11.0.0/24") - - v4 := netip.MustParseAddr("100.64.0.1") - node := types.Node{ - ID: 0, - MachineKey: machineKey.Public(), - NodeKey: nodeKey.Public(), - Hostname: "test", - UserID: user.ID, - RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: ptr.To(pak.ID), - Hostinfo: &tailcfg.Hostinfo{ - RequestTags: []string{"tag:exit"}, - RoutableIPs: []netip.Prefix{defaultRouteV4, defaultRouteV6, route1, route2}, +}`, + routes: []netip.Prefix{ + netip.MustParsePrefix("0.0.0.0/0"), + netip.MustParsePrefix("::/0"), + netip.MustParsePrefix("10.10.0.0/16"), + netip.MustParsePrefix("10.11.0.0/24"), + }, + want: []netip.Prefix{ + netip.MustParsePrefix("::/0"), + netip.MustParsePrefix("10.11.0.0/24"), + netip.MustParsePrefix("10.10.0.0/16"), + netip.MustParsePrefix("0.0.0.0/0"), + }, }, - IPv4: &v4, } - trx := db.DB.Save(&node) - c.Assert(trx.Error, check.IsNil) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adb, err := newTestDB() + assert.NoError(t, err) + pol, err := policy.LoadACLPolicyFromBytes([]byte(tt.acl)) - sendUpdate, err := db.SaveNodeRoutes(&node) - c.Assert(err, check.IsNil) - c.Assert(sendUpdate, check.Equals, false) + assert.NoError(t, err) + assert.NotNil(t, pol) - node0ByID, err := db.GetNodeByID(0) - c.Assert(err, check.IsNil) + user, err := adb.CreateUser("test") + assert.NoError(t, err) - // TODO(kradalby): Check state update - err = db.EnableAutoApprovedRoutes(pol, node0ByID) - c.Assert(err, check.IsNil) + pak, err := adb.CreatePreAuthKey(user.Name, false, false, nil, nil) + assert.NoError(t, err) - enabledRoutes, err := db.GetEnabledRoutes(node0ByID) - c.Assert(err, check.IsNil) - c.Assert(enabledRoutes, check.HasLen, 4) + nodeKey := key.NewNode() + machineKey := key.NewMachine() + + v4 := netip.MustParseAddr("100.64.0.1") + node := types.Node{ + ID: 0, + MachineKey: machineKey.Public(), + NodeKey: nodeKey.Public(), + Hostname: "test", + UserID: user.ID, + RegisterMethod: util.RegisterMethodAuthKey, + AuthKeyID: ptr.To(pak.ID), + Hostinfo: &tailcfg.Hostinfo{ + RequestTags: []string{"tag:exit"}, + RoutableIPs: tt.routes, + }, + IPv4: &v4, + } + + trx := adb.DB.Save(&node) + assert.NoError(t, trx.Error) + + sendUpdate, err := adb.SaveNodeRoutes(&node) + assert.NoError(t, err) + assert.False(t, sendUpdate) + + node0ByID, err := adb.GetNodeByID(0) + assert.NoError(t, err) + + // TODO(kradalby): Check state update + err = adb.EnableAutoApprovedRoutes(pol, node0ByID) + assert.NoError(t, err) + + enabledRoutes, err := adb.GetEnabledRoutes(node0ByID) + assert.NoError(t, err) + assert.Len(t, enabledRoutes, len(tt.want)) + + sort.Slice(enabledRoutes, func(i, j int) bool { + return util.ComparePrefix(enabledRoutes[i], enabledRoutes[j]) > 0 + }) + + if diff := cmp.Diff(tt.want, enabledRoutes, util.Comparers...); diff != "" { + t.Errorf("unexpected enabled routes (-want +got):\n%s", diff) + } + }) + } } func TestEphemeralGarbageCollectorOrder(t *testing.T) { diff --git a/hscontrol/poll.go b/hscontrol/poll.go index b9bf65a2..d7ba682e 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -1,12 +1,10 @@ package hscontrol import ( - "cmp" "context" "fmt" "math/rand/v2" "net/http" - "net/netip" "sort" "strings" "time" @@ -14,6 +12,7 @@ import ( "github.com/juanfont/headscale/hscontrol/db" "github.com/juanfont/headscale/hscontrol/mapper" "github.com/juanfont/headscale/hscontrol/types" + "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "github.com/sasha-s/go-deadlock" xslices "golang.org/x/exp/slices" @@ -742,10 +741,10 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) { newRoutes := new.RoutableIPs sort.Slice(oldRoutes, func(i, j int) bool { - return comparePrefix(oldRoutes[i], oldRoutes[j]) > 0 + return util.ComparePrefix(oldRoutes[i], oldRoutes[j]) > 0 }) sort.Slice(newRoutes, func(i, j int) bool { - return comparePrefix(newRoutes[i], newRoutes[j]) > 0 + return util.ComparePrefix(newRoutes[i], newRoutes[j]) > 0 }) if !xslices.Equal(oldRoutes, newRoutes) { @@ -764,19 +763,3 @@ func hostInfoChanged(old, new *tailcfg.Hostinfo) (bool, bool) { return false, false } - -// TODO(kradalby): Remove after go 1.23, will be in stdlib. -// Compare returns an integer comparing two prefixes. -// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. -// Prefixes sort first by validity (invalid before valid), then -// address family (IPv4 before IPv6), then prefix length, then -// address. -func comparePrefix(p, p2 netip.Prefix) int { - if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 { - return c - } - if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 { - return c - } - return p.Addr().Compare(p2.Addr()) -} diff --git a/hscontrol/util/net.go b/hscontrol/util/net.go index b704c936..c44b7287 100644 --- a/hscontrol/util/net.go +++ b/hscontrol/util/net.go @@ -1,8 +1,10 @@ package util import ( + "cmp" "context" "net" + "net/netip" ) func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) { @@ -10,3 +12,20 @@ func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) { return d.DialContext(ctx, "unix", addr) } + + +// TODO(kradalby): Remove after go 1.24, will be in stdlib. +// Compare returns an integer comparing two prefixes. +// The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. +// Prefixes sort first by validity (invalid before valid), then +// address family (IPv4 before IPv6), then prefix length, then +// address. +func ComparePrefix(p, p2 netip.Prefix) int { + if c := cmp.Compare(p.Addr().BitLen(), p2.Addr().BitLen()); c != 0 { + return c + } + if c := cmp.Compare(p.Bits(), p2.Bits()); c != 0 { + return c + } + return p.Addr().Compare(p2.Addr()) +} diff --git a/integration/route_test.go b/integration/route_test.go index a92258af..0252e702 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" "github.com/juanfont/headscale/hscontrol/policy" "github.com/juanfont/headscale/hscontrol/util" @@ -957,6 +958,95 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) { assert.Equal(t, true, reAdvertisedRoutes[0].GetIsPrimary()) } +func TestAutoApprovedSubRoute2068(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + expectedRoutes := "10.42.7.0/24" + + user := "subroute" + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErrf(t, "failed to create scenario: %s", err) + defer scenario.Shutdown() + + spec := map[string]int{ + user: 1, + } + + err = scenario.CreateHeadscaleEnv(spec, []tsic.Option{tsic.WithTags([]string{"tag:approve"})}, hsic.WithTestName("clienableroute"), hsic.WithACLPolicy( + &policy.ACLPolicy{ + ACLs: []policy.ACL{ + { + Action: "accept", + Sources: []string{"*"}, + Destinations: []string{"*:*"}, + }, + }, + TagOwners: map[string][]string{ + "tag:approve": {user}, + }, + AutoApprovers: policy.AutoApprovers{ + Routes: map[string][]string{ + "10.42.0.0/16": {"tag:approve"}, + }, + }, + }, + )) + assertNoErrHeadscaleEnv(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + headscale, err := scenario.Headscale() + assertNoErrGetHeadscale(t, err) + + subRouter1 := allClients[0] + + // Initially advertise route + command := []string{ + "tailscale", + "set", + "--advertise-routes=" + expectedRoutes, + } + _, _, err = subRouter1.Execute(command) + assertNoErrf(t, "failed to advertise route: %s", err) + + time.Sleep(10 * time.Second) + + var routes []*v1.Route + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "routes", + "list", + "--output", + "json", + }, + &routes, + ) + assertNoErr(t, err) + assert.Len(t, routes, 1) + + want := []*v1.Route{ + { + Id: 1, + Prefix: expectedRoutes, + Advertised: true, + Enabled: true, + IsPrimary: true, + }, + } + + if diff := cmp.Diff(want, routes, cmpopts.IgnoreUnexported(v1.Route{}), cmpopts.IgnoreFields(v1.Route{}, "Node", "CreatedAt", "UpdatedAt", "DeletedAt")); diff != "" { + t.Errorf("unexpected routes (-want +got):\n%s", diff) + } +} + // TestSubnetRouteACL verifies that Subnet routes are distributed // as expected when ACLs are activated. // It implements the issue from From 8a3a0fee3ccbca7dd67b0d2965b523c8b6cb5451 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sat, 7 Sep 2024 09:23:58 +0200 Subject: [PATCH 35/60] Only load needed part of configuration (#2109) --- .gitignore | 1 + CHANGELOG.md | 2 + cmd/headscale/cli/api_key.go | 33 +------ cmd/headscale/cli/configtest.go | 2 +- cmd/headscale/cli/debug.go | 14 +-- cmd/headscale/cli/nodes.go | 34 ++----- cmd/headscale/cli/policy.go | 27 +++--- cmd/headscale/cli/preauthkeys.go | 28 +----- cmd/headscale/cli/root.go | 16 ++-- cmd/headscale/cli/routes.go | 40 +------- cmd/headscale/cli/{server.go => serve.go} | 2 +- cmd/headscale/cli/users.go | 22 +---- cmd/headscale/cli/utils.go | 27 +++--- cmd/headscale/headscale_test.go | 58 ------------ hscontrol/grpcv1.go | 8 +- hscontrol/types/config.go | 110 ++++++++++++---------- hscontrol/types/config_test.go | 68 +++++++++++-- integration/cli_test.go | 28 ++---- 18 files changed, 196 insertions(+), 324 deletions(-) rename cmd/headscale/cli/{server.go => serve.go} (92%) diff --git a/.gitignore b/.gitignore index f6e506bc..1662d7f2 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ dist/ /headscale config.json config.yaml +config*.yaml derp.yaml *.hujson *.key diff --git a/CHANGELOG.md b/CHANGELOG.md index 76982608..91aed9ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,6 +72,8 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Add APIs for managing headscale policy. [#1792](https://github.com/juanfont/headscale/pull/1792) - Fix for registering nodes using preauthkeys when running on a postgres database in a non-UTC timezone. [#764](https://github.com/juanfont/headscale/issues/764) - Make sure integration tests cover postgres for all scenarios +- CLI commands (all except `serve`) only requires minimal configuration, no more errors or warnings from unset settings [#2109](https://github.com/juanfont/headscale/pull/2109) +- CLI results are now concistently sent to stdout and errors to stderr [#2109](https://github.com/juanfont/headscale/pull/2109) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/api_key.go b/cmd/headscale/cli/api_key.go index 372ec390..bd839b7b 100644 --- a/cmd/headscale/cli/api_key.go +++ b/cmd/headscale/cli/api_key.go @@ -54,7 +54,7 @@ var listAPIKeys = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -67,14 +67,10 @@ var listAPIKeys = &cobra.Command{ fmt.Sprintf("Error getting the list of keys: %s", err), output, ) - - return } if output != "" { SuccessOutput(response.GetApiKeys(), "", output) - - return } tableData := pterm.TableData{ @@ -102,8 +98,6 @@ var listAPIKeys = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -119,9 +113,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`, Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - log.Trace(). - Msg("Preparing to create ApiKey") - request := &v1.CreateApiKeyRequest{} durationStr, _ := cmd.Flags().GetString("expiration") @@ -133,19 +124,13 @@ If you loose a key, create a new one and revoke (expire) the old one.`, fmt.Sprintf("Could not parse duration: %s\n", err), output, ) - - return } expiration := time.Now().UTC().Add(time.Duration(duration)) - log.Trace(). - Dur("expiration", time.Duration(duration)). - Msg("expiration has been set") - request.Expiration = timestamppb.New(expiration) - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -156,8 +141,6 @@ If you loose a key, create a new one and revoke (expire) the old one.`, fmt.Sprintf("Cannot create Api Key: %s\n", err), output, ) - - return } SuccessOutput(response.GetApiKey(), response.GetApiKey(), output) @@ -178,11 +161,9 @@ var expireAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Error getting prefix from CLI flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -197,8 +178,6 @@ var expireAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot expire Api Key: %s\n", err), output, ) - - return } SuccessOutput(response, "Key expired", output) @@ -219,11 +198,9 @@ var deleteAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Error getting prefix from CLI flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -238,8 +215,6 @@ var deleteAPIKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot delete Api Key: %s\n", err), output, ) - - return } SuccessOutput(response, "Key deleted", output) diff --git a/cmd/headscale/cli/configtest.go b/cmd/headscale/cli/configtest.go index 72744a7b..d469885b 100644 --- a/cmd/headscale/cli/configtest.go +++ b/cmd/headscale/cli/configtest.go @@ -14,7 +14,7 @@ var configTestCmd = &cobra.Command{ Short: "Test the configuration.", Long: "Run a test of the configuration and exit.", Run: func(cmd *cobra.Command, args []string) { - _, err := getHeadscaleApp() + _, err := newHeadscaleServerWithConfig() if err != nil { log.Fatal().Caller().Err(err).Msg("Error initializing") } diff --git a/cmd/headscale/cli/debug.go b/cmd/headscale/cli/debug.go index 054fc07f..72cde32d 100644 --- a/cmd/headscale/cli/debug.go +++ b/cmd/headscale/cli/debug.go @@ -64,11 +64,9 @@ var createNodeCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -79,8 +77,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting node from flag: %s", err), output, ) - - return } machineKey, err := cmd.Flags().GetString("key") @@ -90,8 +86,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting key from flag: %s", err), output, ) - - return } var mkey key.MachinePublic @@ -102,8 +96,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Failed to parse machine key from flag: %s", err), output, ) - - return } routes, err := cmd.Flags().GetStringSlice("route") @@ -113,8 +105,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting routes from flag: %s", err), output, ) - - return } request := &v1.DebugCreateNodeRequest{ @@ -131,8 +121,6 @@ var createNodeCmd = &cobra.Command{ fmt.Sprintf("Cannot create node: %s", status.Convert(err).Message()), output, ) - - return } SuccessOutput(response.GetNode(), "Node created", output) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 4de7b969..b9e97a33 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -116,11 +116,9 @@ var registerNodeCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -131,8 +129,6 @@ var registerNodeCmd = &cobra.Command{ fmt.Sprintf("Error getting node key from flag: %s", err), output, ) - - return } request := &v1.RegisterNodeRequest{ @@ -150,8 +146,6 @@ var registerNodeCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput( @@ -169,17 +163,13 @@ var listNodesCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } showTags, err := cmd.Flags().GetBool("tags") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting tags flag: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -194,21 +184,15 @@ var listNodesCmd = &cobra.Command{ fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetNodes(), "", output) - - return } tableData, err := nodesToPtables(user, showTags, response.GetNodes()) if err != nil { ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) - - return } err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() @@ -218,8 +202,6 @@ var listNodesCmd = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -243,7 +225,7 @@ var expireNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -286,7 +268,7 @@ var renameNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -335,7 +317,7 @@ var deleteNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -435,7 +417,7 @@ var moveNodeCmd = &cobra.Command{ return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -508,7 +490,7 @@ be assigned to nodes.`, return } if confirm { - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -681,7 +663,7 @@ var tagCmd = &cobra.Command{ Aliases: []string{"tags", "t"}, Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() diff --git a/cmd/headscale/cli/policy.go b/cmd/headscale/cli/policy.go index 00c4566d..d1349b5a 100644 --- a/cmd/headscale/cli/policy.go +++ b/cmd/headscale/cli/policy.go @@ -1,6 +1,7 @@ package cli import ( + "fmt" "io" "os" @@ -30,7 +31,8 @@ var getPolicy = &cobra.Command{ Short: "Print the current ACL Policy", Aliases: []string{"show", "view", "fetch"}, Run: func(cmd *cobra.Command, args []string) { - ctx, client, conn, cancel := getHeadscaleCLIClient() + output, _ := cmd.Flags().GetString("output") + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -38,13 +40,13 @@ var getPolicy = &cobra.Command{ response, err := client.GetPolicy(ctx, request) if err != nil { - log.Fatal().Err(err).Msg("Failed to get the policy") - - return + ErrorOutput(err, fmt.Sprintf("Failed loading ACL Policy: %s", err), output) } // TODO(pallabpain): Maybe print this better? - SuccessOutput("", response.GetPolicy(), "hujson") + // This does not pass output as we dont support yaml, json or json-line + // output for this command. It is HuJSON already. + SuccessOutput("", response.GetPolicy(), "") }, } @@ -56,33 +58,28 @@ var setPolicy = &cobra.Command{ This command only works when the acl.policy_mode is set to "db", and the policy will be stored in the database.`, Aliases: []string{"put", "update"}, Run: func(cmd *cobra.Command, args []string) { + output, _ := cmd.Flags().GetString("output") policyPath, _ := cmd.Flags().GetString("file") f, err := os.Open(policyPath) if err != nil { - log.Fatal().Err(err).Msg("Error opening the policy file") - - return + ErrorOutput(err, fmt.Sprintf("Error opening the policy file: %s", err), output) } defer f.Close() policyBytes, err := io.ReadAll(f) if err != nil { - log.Fatal().Err(err).Msg("Error reading the policy file") - - return + ErrorOutput(err, fmt.Sprintf("Error reading the policy file: %s", err), output) } request := &v1.SetPolicyRequest{Policy: string(policyBytes)} - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() if _, err := client.SetPolicy(ctx, request); err != nil { - log.Fatal().Err(err).Msg("Failed to set ACL Policy") - - return + ErrorOutput(err, fmt.Sprintf("Failed to set ACL Policy: %s", err), output) } SuccessOutput(nil, "Policy updated.", "") diff --git a/cmd/headscale/cli/preauthkeys.go b/cmd/headscale/cli/preauthkeys.go index cc3b1b76..0074e029 100644 --- a/cmd/headscale/cli/preauthkeys.go +++ b/cmd/headscale/cli/preauthkeys.go @@ -60,11 +60,9 @@ var listPreAuthKeys = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -85,8 +83,6 @@ var listPreAuthKeys = &cobra.Command{ if output != "" { SuccessOutput(response.GetPreAuthKeys(), "", output) - - return } tableData := pterm.TableData{ @@ -134,8 +130,6 @@ var listPreAuthKeys = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -150,20 +144,12 @@ var createPreAuthKeyCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } reusable, _ := cmd.Flags().GetBool("reusable") ephemeral, _ := cmd.Flags().GetBool("ephemeral") tags, _ := cmd.Flags().GetStringSlice("tags") - log.Trace(). - Bool("reusable", reusable). - Bool("ephemeral", ephemeral). - Str("user", user). - Msg("Preparing to create preauthkey") - request := &v1.CreatePreAuthKeyRequest{ User: user, Reusable: reusable, @@ -180,8 +166,6 @@ var createPreAuthKeyCmd = &cobra.Command{ fmt.Sprintf("Could not parse duration: %s\n", err), output, ) - - return } expiration := time.Now().UTC().Add(time.Duration(duration)) @@ -192,7 +176,7 @@ var createPreAuthKeyCmd = &cobra.Command{ request.Expiration = timestamppb.New(expiration) - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -203,8 +187,6 @@ var createPreAuthKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot create Pre Auth Key: %s\n", err), output, ) - - return } SuccessOutput(response.GetPreAuthKey(), response.GetPreAuthKey().GetKey(), output) @@ -227,11 +209,9 @@ var expirePreAuthKeyCmd = &cobra.Command{ user, err := cmd.Flags().GetString("user") if err != nil { ErrorOutput(err, fmt.Sprintf("Error getting user: %s", err), output) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -247,8 +227,6 @@ var expirePreAuthKeyCmd = &cobra.Command{ fmt.Sprintf("Cannot expire Pre Auth Key: %s\n", err), output, ) - - return } SuccessOutput(response, "Key expired", output) diff --git a/cmd/headscale/cli/root.go b/cmd/headscale/cli/root.go index b0d9500e..7bac79ce 100644 --- a/cmd/headscale/cli/root.go +++ b/cmd/headscale/cli/root.go @@ -9,6 +9,7 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/tcnksm/go-latest" ) @@ -49,11 +50,6 @@ func initConfig() { } } - cfg, err := types.GetHeadscaleConfig() - if err != nil { - log.Fatal().Err(err).Msg("Failed to read headscale configuration") - } - machineOutput := HasMachineOutputFlag() // If the user has requested a "node" readable format, @@ -62,11 +58,13 @@ func initConfig() { zerolog.SetGlobalLevel(zerolog.Disabled) } - if cfg.Log.Format == types.JSONLogFormat { - log.Logger = log.Output(os.Stdout) - } + // logFormat := viper.GetString("log.format") + // if logFormat == types.JSONLogFormat { + // log.Logger = log.Output(os.Stdout) + // } - if !cfg.DisableUpdateCheck && !machineOutput { + disableUpdateCheck := viper.GetBool("disable_check_updates") + if !disableUpdateCheck && !machineOutput { if (runtime.GOOS == "linux" || runtime.GOOS == "darwin") && Version != "dev" { githubTag := &latest.GithubTag{ diff --git a/cmd/headscale/cli/routes.go b/cmd/headscale/cli/routes.go index 86ef295c..96227b31 100644 --- a/cmd/headscale/cli/routes.go +++ b/cmd/headscale/cli/routes.go @@ -64,11 +64,9 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -82,14 +80,10 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Cannot get nodes: %s", status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetRoutes(), "", output) - - return } routes = response.GetRoutes() @@ -103,14 +97,10 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Cannot get routes for node %d: %s", machineID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetRoutes(), "", output) - - return } routes = response.GetRoutes() @@ -119,8 +109,6 @@ var listRoutesCmd = &cobra.Command{ tableData := routesToPtables(routes) if err != nil { ErrorOutput(err, fmt.Sprintf("Error converting to table: %s", err), output) - - return } err = pterm.DefaultTable.WithHasHeader().WithData(tableData).Render() @@ -130,8 +118,6 @@ var listRoutesCmd = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -150,11 +136,9 @@ var enableRouteCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -167,14 +151,10 @@ var enableRouteCmd = &cobra.Command{ fmt.Sprintf("Cannot enable route %d: %s", routeID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response, "", output) - - return } }, } @@ -193,11 +173,9 @@ var disableRouteCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -210,14 +188,10 @@ var disableRouteCmd = &cobra.Command{ fmt.Sprintf("Cannot disable route %d: %s", routeID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response, "", output) - - return } }, } @@ -236,11 +210,9 @@ var deleteRouteCmd = &cobra.Command{ fmt.Sprintf("Error getting machine id from flag: %s", err), output, ) - - return } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -253,14 +225,10 @@ var deleteRouteCmd = &cobra.Command{ fmt.Sprintf("Cannot delete route %d: %s", routeID, status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response, "", output) - - return } }, } diff --git a/cmd/headscale/cli/server.go b/cmd/headscale/cli/serve.go similarity index 92% rename from cmd/headscale/cli/server.go rename to cmd/headscale/cli/serve.go index a1d19600..9f0fa35e 100644 --- a/cmd/headscale/cli/server.go +++ b/cmd/headscale/cli/serve.go @@ -16,7 +16,7 @@ var serveCmd = &cobra.Command{ return nil }, Run: func(cmd *cobra.Command, args []string) { - app, err := getHeadscaleApp() + app, err := newHeadscaleServerWithConfig() if err != nil { log.Fatal().Caller().Err(err).Msg("Error initializing") } diff --git a/cmd/headscale/cli/users.go b/cmd/headscale/cli/users.go index e6463d6f..d04d7568 100644 --- a/cmd/headscale/cli/users.go +++ b/cmd/headscale/cli/users.go @@ -44,7 +44,7 @@ var createUserCmd = &cobra.Command{ userName := args[0] - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -63,8 +63,6 @@ var createUserCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput(response.GetUser(), "User created", output) @@ -91,7 +89,7 @@ var destroyUserCmd = &cobra.Command{ Name: userName, } - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -102,8 +100,6 @@ var destroyUserCmd = &cobra.Command{ fmt.Sprintf("Error: %s", status.Convert(err).Message()), output, ) - - return } confirm := false @@ -134,8 +130,6 @@ var destroyUserCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput(response, "User destroyed", output) } else { @@ -151,7 +145,7 @@ var listUsersCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -164,14 +158,10 @@ var listUsersCmd = &cobra.Command{ fmt.Sprintf("Cannot get users: %s", status.Convert(err).Message()), output, ) - - return } if output != "" { SuccessOutput(response.GetUsers(), "", output) - - return } tableData := pterm.TableData{{"ID", "Name", "Created"}} @@ -192,8 +182,6 @@ var listUsersCmd = &cobra.Command{ fmt.Sprintf("Failed to render pterm table: %s", err), output, ) - - return } }, } @@ -213,7 +201,7 @@ var renameUserCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { output, _ := cmd.Flags().GetString("output") - ctx, client, conn, cancel := getHeadscaleCLIClient() + ctx, client, conn, cancel := newHeadscaleCLIWithConfig() defer cancel() defer conn.Close() @@ -232,8 +220,6 @@ var renameUserCmd = &cobra.Command{ ), output, ) - - return } SuccessOutput(response.GetUser(), "User renamed", output) diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index 409e3dc4..ff1137be 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -23,8 +23,8 @@ const ( SocketWritePermissions = 0o666 ) -func getHeadscaleApp() (*hscontrol.Headscale, error) { - cfg, err := types.GetHeadscaleConfig() +func newHeadscaleServerWithConfig() (*hscontrol.Headscale, error) { + cfg, err := types.LoadServerConfig() if err != nil { return nil, fmt.Errorf( "failed to load configuration while creating headscale instance: %w", @@ -40,8 +40,8 @@ func getHeadscaleApp() (*hscontrol.Headscale, error) { return app, nil } -func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { - cfg, err := types.GetHeadscaleConfig() +func newHeadscaleCLIWithConfig() (context.Context, v1.HeadscaleServiceClient, *grpc.ClientConn, context.CancelFunc) { + cfg, err := types.LoadCLIConfig() if err != nil { log.Fatal(). Err(err). @@ -130,7 +130,7 @@ func getHeadscaleCLIClient() (context.Context, v1.HeadscaleServiceClient, *grpc. return ctx, client, conn, cancel } -func SuccessOutput(result interface{}, override string, outputFormat string) { +func output(result interface{}, override string, outputFormat string) string { var jsonBytes []byte var err error switch outputFormat { @@ -151,21 +151,26 @@ func SuccessOutput(result interface{}, override string, outputFormat string) { } default: // nolint - fmt.Println(override) - - return + return override } - // nolint - fmt.Println(string(jsonBytes)) + return string(jsonBytes) } +// SuccessOutput prints the result to stdout and exits with status code 0. +func SuccessOutput(result interface{}, override string, outputFormat string) { + fmt.Println(output(result, override, outputFormat)) + os.Exit(0) +} + +// ErrorOutput prints an error message to stderr and exits with status code 1. func ErrorOutput(errResult error, override string, outputFormat string) { type errOutput struct { Error string `json:"error"` } - SuccessOutput(errOutput{errResult.Error()}, override, outputFormat) + fmt.Fprintf(os.Stderr, "%s\n", output(errOutput{errResult.Error()}, override, outputFormat)) + os.Exit(1) } func HasMachineOutputFlag() bool { diff --git a/cmd/headscale/headscale_test.go b/cmd/headscale/headscale_test.go index 580caf17..00c4a276 100644 --- a/cmd/headscale/headscale_test.go +++ b/cmd/headscale/headscale_test.go @@ -4,7 +4,6 @@ import ( "io/fs" "os" "path/filepath" - "strings" "testing" "github.com/juanfont/headscale/hscontrol/types" @@ -113,60 +112,3 @@ func (*Suite) TestConfigLoading(c *check.C) { c.Assert(viper.GetBool("logtail.enabled"), check.Equals, false) c.Assert(viper.GetBool("randomize_client_port"), check.Equals, false) } - -func writeConfig(c *check.C, tmpDir string, configYaml []byte) { - // Populate a custom config file - configFile := filepath.Join(tmpDir, "config.yaml") - err := os.WriteFile(configFile, configYaml, 0o600) - if err != nil { - c.Fatalf("Couldn't write file %s", configFile) - } -} - -func (*Suite) TestTLSConfigValidation(c *check.C) { - tmpDir, err := os.MkdirTemp("", "headscale") - if err != nil { - c.Fatal(err) - } - // defer os.RemoveAll(tmpDir) - configYaml := []byte(`--- -tls_letsencrypt_hostname: example.com -tls_letsencrypt_challenge_type: "" -tls_cert_path: abc.pem -noise: - private_key_path: noise_private.key`) - writeConfig(c, tmpDir, configYaml) - - // Check configuration validation errors (1) - err = types.LoadConfig(tmpDir, false) - c.Assert(err, check.NotNil) - // check.Matches can not handle multiline strings - tmp := strings.ReplaceAll(err.Error(), "\n", "***") - c.Assert( - tmp, - check.Matches, - ".*Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both.*", - ) - c.Assert( - tmp, - check.Matches, - ".*Fatal config error: the only supported values for tls_letsencrypt_challenge_type are.*", - ) - c.Assert( - tmp, - check.Matches, - ".*Fatal config error: server_url must start with https:// or http://.*", - ) - - // Check configuration validation errors (2) - configYaml = []byte(`--- -noise: - private_key_path: noise_private.key -server_url: http://127.0.0.1:8080 -tls_letsencrypt_hostname: example.com -tls_letsencrypt_challenge_type: TLS-ALPN-01 -`) - writeConfig(c, tmpDir, configYaml) - err = types.LoadConfig(tmpDir, false) - c.Assert(err, check.IsNil) -} diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 83048bec..3f985d98 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -684,7 +684,7 @@ func (api headscaleV1APIServer) GetPolicy( case types.PolicyModeDB: p, err := api.h.db.GetPolicy() if err != nil { - return nil, err + return nil, fmt.Errorf("loading ACL from database: %w", err) } return &v1.GetPolicyResponse{ @@ -696,20 +696,20 @@ func (api headscaleV1APIServer) GetPolicy( absPath := util.AbsolutePathFromConfigPath(api.h.cfg.Policy.Path) f, err := os.Open(absPath) if err != nil { - return nil, err + return nil, fmt.Errorf("reading policy from path %q: %w", absPath, err) } defer f.Close() b, err := io.ReadAll(f) if err != nil { - return nil, err + return nil, fmt.Errorf("reading policy from file: %w", err) } return &v1.GetPolicyResponse{Policy: string(b)}, nil } - return nil, nil + return nil, fmt.Errorf("no supported policy mode found in configuration, policy.mode: %q", api.h.cfg.Policy.Mode) } func (api headscaleV1APIServer) SetPolicy( diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 0b7d63b7..8767077e 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -212,6 +212,12 @@ type Tuning struct { NodeMapSessionBufferedChanSize int } +// LoadConfig prepares and loads the Headscale configuration into Viper. +// This means it sets the default values, reads the configuration file and +// environment variables, and handles deprecated configuration options. +// It has to be called before LoadServerConfig and LoadCLIConfig. +// The configuration is not validated and the caller should check for errors +// using a validation function. func LoadConfig(path string, isFile bool) error { if isFile { viper.SetConfigFile(path) @@ -284,14 +290,14 @@ func LoadConfig(path string, isFile bool) error { viper.SetDefault("prefixes.allocation", string(IPAllocationStrategySequential)) - if IsCLIConfigured() { - return nil - } - if err := viper.ReadInConfig(); err != nil { return fmt.Errorf("fatal error reading config file: %w", err) } + return nil +} + +func validateServerConfig() error { depr := deprecator{ warns: make(set.Set[string]), fatals: make(set.Set[string]), @@ -360,12 +366,12 @@ func LoadConfig(path string, isFile bool) error { if errorText != "" { // nolint return errors.New(strings.TrimSuffix(errorText, "\n")) - } else { - return nil } + + return nil } -func GetTLSConfig() TLSConfig { +func tlsConfig() TLSConfig { return TLSConfig{ LetsEncrypt: LetsEncryptConfig{ Hostname: viper.GetString("tls_letsencrypt_hostname"), @@ -384,7 +390,7 @@ func GetTLSConfig() TLSConfig { } } -func GetDERPConfig() DERPConfig { +func derpConfig() DERPConfig { serverEnabled := viper.GetBool("derp.server.enabled") serverRegionID := viper.GetInt("derp.server.region_id") serverRegionCode := viper.GetString("derp.server.region_code") @@ -445,7 +451,7 @@ func GetDERPConfig() DERPConfig { } } -func GetLogTailConfig() LogTailConfig { +func logtailConfig() LogTailConfig { enabled := viper.GetBool("logtail.enabled") return LogTailConfig{ @@ -453,7 +459,7 @@ func GetLogTailConfig() LogTailConfig { } } -func GetPolicyConfig() PolicyConfig { +func policyConfig() PolicyConfig { policyPath := viper.GetString("policy.path") policyMode := viper.GetString("policy.mode") @@ -463,7 +469,7 @@ func GetPolicyConfig() PolicyConfig { } } -func GetLogConfig() LogConfig { +func logConfig() LogConfig { logLevelStr := viper.GetString("log.level") logLevel, err := zerolog.ParseLevel(logLevelStr) if err != nil { @@ -473,9 +479,9 @@ func GetLogConfig() LogConfig { logFormatOpt := viper.GetString("log.format") var logFormat string switch logFormatOpt { - case "json": + case JSONLogFormat: logFormat = JSONLogFormat - case "text": + case TextLogFormat: logFormat = TextLogFormat case "": logFormat = TextLogFormat @@ -491,7 +497,7 @@ func GetLogConfig() LogConfig { } } -func GetDatabaseConfig() DatabaseConfig { +func databaseConfig() DatabaseConfig { debug := viper.GetBool("database.debug") type_ := viper.GetString("database.type") @@ -543,7 +549,7 @@ func GetDatabaseConfig() DatabaseConfig { } } -func DNS() (DNSConfig, error) { +func dns() (DNSConfig, error) { var dns DNSConfig // TODO: Use this instead of manually getting settings when @@ -575,12 +581,12 @@ func DNS() (DNSConfig, error) { return dns, nil } -// GlobalResolvers returns the global DNS resolvers +// 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 { +func (d *DNSConfig) globalResolvers() []*dnstype.Resolver { var resolvers []*dnstype.Resolver for _, nsStr := range d.Nameservers.Global { @@ -613,11 +619,11 @@ func (d *DNSConfig) GlobalResolvers() []*dnstype.Resolver { return resolvers } -// SplitResolvers returns a map of domain to DNS resolvers. +// splitResolvers returns a map of domain to DNS resolvers. // If a nameserver is a valid IP, it will be used as a regular resolver. // If a nameserver is a valid URL, it will be used as a DoH resolver. // If a nameserver is neither a valid URL nor a valid IP, it will be ignored. -func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { +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 @@ -653,7 +659,7 @@ func (d *DNSConfig) SplitResolvers() map[string][]*dnstype.Resolver { return routes } -func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { +func dnsToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { cfg := tailcfg.DNSConfig{} if dns.BaseDomain == "" && dns.MagicDNS { @@ -662,9 +668,9 @@ func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { cfg.Proxied = dns.MagicDNS cfg.ExtraRecords = dns.ExtraRecords - cfg.Resolvers = dns.GlobalResolvers() + cfg.Resolvers = dns.globalResolvers() - routes := dns.SplitResolvers() + routes := dns.splitResolvers() cfg.Routes = routes if dns.BaseDomain != "" { cfg.Domains = []string{dns.BaseDomain} @@ -674,7 +680,7 @@ func DNSToTailcfgDNS(dns DNSConfig) *tailcfg.DNSConfig { return &cfg } -func PrefixV4() (*netip.Prefix, error) { +func prefixV4() (*netip.Prefix, error) { prefixV4Str := viper.GetString("prefixes.v4") if prefixV4Str == "" { @@ -698,7 +704,7 @@ func PrefixV4() (*netip.Prefix, error) { return &prefixV4, nil } -func PrefixV6() (*netip.Prefix, error) { +func prefixV6() (*netip.Prefix, error) { prefixV6Str := viper.GetString("prefixes.v6") if prefixV6Str == "" { @@ -723,27 +729,37 @@ func PrefixV6() (*netip.Prefix, error) { return &prefixV6, nil } -func GetHeadscaleConfig() (*Config, error) { - if IsCLIConfigured() { - return &Config{ - CLI: CLIConfig{ - Address: viper.GetString("cli.address"), - APIKey: viper.GetString("cli.api_key"), - Timeout: viper.GetDuration("cli.timeout"), - Insecure: viper.GetBool("cli.insecure"), - }, - }, nil +// LoadCLIConfig returns the needed configuration for the CLI client +// of Headscale to connect to a Headscale server. +func LoadCLIConfig() (*Config, error) { + return &Config{ + DisableUpdateCheck: viper.GetBool("disable_check_updates"), + UnixSocket: viper.GetString("unix_socket"), + CLI: CLIConfig{ + Address: viper.GetString("cli.address"), + APIKey: viper.GetString("cli.api_key"), + Timeout: viper.GetDuration("cli.timeout"), + Insecure: viper.GetBool("cli.insecure"), + }, + }, nil +} + +// LoadServerConfig returns the full Headscale configuration to +// host a Headscale server. This is called as part of `headscale serve`. +func LoadServerConfig() (*Config, error) { + if err := validateServerConfig(); err != nil { + return nil, err } - logConfig := GetLogConfig() + logConfig := logConfig() zerolog.SetGlobalLevel(logConfig.Level) - prefix4, err := PrefixV4() + prefix4, err := prefixV4() if err != nil { return nil, err } - prefix6, err := PrefixV6() + prefix6, err := prefixV6() if err != nil { return nil, err } @@ -763,13 +779,13 @@ func GetHeadscaleConfig() (*Config, error) { return nil, fmt.Errorf("config error, prefixes.allocation is set to %s, which is not a valid strategy, allowed options: %s, %s", allocStr, IPAllocationStrategySequential, IPAllocationStrategyRandom) } - dnsConfig, err := DNS() + dnsConfig, err := dns() if err != nil { return nil, err } - derpConfig := GetDERPConfig() - logTailConfig := GetLogTailConfig() + derpConfig := derpConfig() + logTailConfig := logtailConfig() randomizeClientPort := viper.GetBool("randomize_client_port") oidcClientSecret := viper.GetString("oidc.client_secret") @@ -806,7 +822,7 @@ func GetHeadscaleConfig() (*Config, error) { MetricsAddr: viper.GetString("metrics_listen_addr"), GRPCAddr: viper.GetString("grpc_listen_addr"), GRPCAllowInsecure: viper.GetBool("grpc_allow_insecure"), - DisableUpdateCheck: viper.GetBool("disable_check_updates"), + DisableUpdateCheck: false, PrefixV4: prefix4, PrefixV6: prefix6, @@ -823,11 +839,11 @@ func GetHeadscaleConfig() (*Config, error) { "ephemeral_node_inactivity_timeout", ), - Database: GetDatabaseConfig(), + Database: databaseConfig(), - TLS: GetTLSConfig(), + TLS: tlsConfig(), - DNSConfig: DNSToTailcfgDNS(dnsConfig), + DNSConfig: dnsToTailcfgDNS(dnsConfig), DNSUserNameInMagicDNS: dnsConfig.UserNameInMagicDNS, ACMEEmail: viper.GetString("acme_email"), @@ -870,7 +886,7 @@ func GetHeadscaleConfig() (*Config, error) { LogTail: logTailConfig, RandomizeClientPort: randomizeClientPort, - Policy: GetPolicyConfig(), + Policy: policyConfig(), CLI: CLIConfig{ Address: viper.GetString("cli.address"), @@ -890,10 +906,6 @@ func GetHeadscaleConfig() (*Config, error) { }, nil } -func IsCLIConfigured() bool { - return viper.GetString("cli.address") != "" && viper.GetString("cli.api_key") != "" -} - type deprecator struct { warns set.Set[string] fatals set.Set[string] diff --git a/hscontrol/types/config_test.go b/hscontrol/types/config_test.go index 2b36e45c..e6e8d6c2 100644 --- a/hscontrol/types/config_test.go +++ b/hscontrol/types/config_test.go @@ -1,6 +1,8 @@ package types import ( + "os" + "path/filepath" "testing" "github.com/google/go-cmp/cmp" @@ -22,7 +24,7 @@ func TestReadConfig(t *testing.T) { name: "unmarshal-dns-full-config", configPath: "testdata/dns_full.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } @@ -48,12 +50,12 @@ func TestReadConfig(t *testing.T) { name: "dns-to-tailcfg.DNSConfig", configPath: "testdata/dns_full.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } - return DNSToTailcfgDNS(dns), nil + return dnsToTailcfgDNS(dns), nil }, want: &tailcfg.DNSConfig{ Proxied: true, @@ -79,7 +81,7 @@ func TestReadConfig(t *testing.T) { name: "unmarshal-dns-full-no-magic", configPath: "testdata/dns_full_no_magic.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } @@ -105,12 +107,12 @@ func TestReadConfig(t *testing.T) { name: "dns-to-tailcfg.DNSConfig", configPath: "testdata/dns_full_no_magic.yaml", setup: func(t *testing.T) (any, error) { - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } - return DNSToTailcfgDNS(dns), nil + return dnsToTailcfgDNS(dns), nil }, want: &tailcfg.DNSConfig{ Proxied: false, @@ -136,7 +138,7 @@ func TestReadConfig(t *testing.T) { name: "base-domain-in-server-url-err", configPath: "testdata/base-domain-in-server-url.yaml", setup: func(t *testing.T) (any, error) { - return GetHeadscaleConfig() + return LoadServerConfig() }, 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.", @@ -145,7 +147,7 @@ func TestReadConfig(t *testing.T) { 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() + cfg, err := LoadServerConfig() if err != nil { return nil, err } @@ -165,7 +167,7 @@ func TestReadConfig(t *testing.T) { name: "policy-path-is-loaded", configPath: "testdata/policy-path-is-loaded.yaml", setup: func(t *testing.T) (any, error) { - cfg, err := GetHeadscaleConfig() + cfg, err := LoadServerConfig() if err != nil { return nil, err } @@ -245,7 +247,7 @@ func TestReadConfigFromEnv(t *testing.T) { setup: func(t *testing.T) (any, error) { t.Logf("all settings: %#v", viper.AllSettings()) - dns, err := DNS() + dns, err := dns() if err != nil { return nil, err } @@ -289,3 +291,49 @@ func TestReadConfigFromEnv(t *testing.T) { }) } } + +func TestTLSConfigValidation(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "headscale") + if err != nil { + t.Fatal(err) + } + // defer os.RemoveAll(tmpDir) + configYaml := []byte(`--- +tls_letsencrypt_hostname: example.com +tls_letsencrypt_challenge_type: "" +tls_cert_path: abc.pem +noise: + private_key_path: noise_private.key`) + + // Populate a custom config file + configFilePath := filepath.Join(tmpDir, "config.yaml") + err = os.WriteFile(configFilePath, configYaml, 0o600) + if err != nil { + t.Fatalf("Couldn't write file %s", configFilePath) + } + + // Check configuration validation errors (1) + err = LoadConfig(tmpDir, false) + assert.NoError(t, err) + + err = validateServerConfig() + assert.Error(t, err) + assert.Contains(t, err.Error(), "Fatal config error: set either tls_letsencrypt_hostname or tls_cert_path/tls_key_path, not both") + assert.Contains(t, err.Error(), "Fatal config error: the only supported values for tls_letsencrypt_challenge_type are") + assert.Contains(t, err.Error(), "Fatal config error: server_url must start with https:// or http://") + + // Check configuration validation errors (2) + configYaml = []byte(`--- +noise: + private_key_path: noise_private.key +server_url: http://127.0.0.1:8080 +tls_letsencrypt_hostname: example.com +tls_letsencrypt_challenge_type: TLS-ALPN-01 +`) + err = os.WriteFile(configFilePath, configYaml, 0o600) + if err != nil { + t.Fatalf("Couldn't write file %s", configFilePath) + } + err = LoadConfig(tmpDir, false) + assert.NoError(t, err) +} diff --git a/integration/cli_test.go b/integration/cli_test.go index 9e7d179f..fd7a8c1b 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "testing" "time" @@ -735,13 +736,7 @@ func TestNodeTagCommand(t *testing.T) { assert.Equal(t, []string{"tag:test"}, node.GetForcedTags()) - // try to set a wrong tag and retrieve the error - type errOutput struct { - Error string `json:"error"` - } - var errorOutput errOutput - err = executeAndUnmarshal( - headscale, + _, err = headscale.Execute( []string{ "headscale", "nodes", @@ -750,10 +745,8 @@ func TestNodeTagCommand(t *testing.T) { "-t", "wrong-tag", "--output", "json", }, - &errorOutput, ) - assert.Nil(t, err) - assert.Contains(t, errorOutput.Error, "tag must start with the string 'tag:'") + assert.ErrorContains(t, err, "tag must start with the string 'tag:'") // Test list all nodes after added seconds resultMachines := make([]*v1.Node, len(machineKeys)) @@ -1398,18 +1391,17 @@ func TestNodeRenameCommand(t *testing.T) { assert.Contains(t, listAllAfterRename[4].GetGivenName(), "node-5") // Test failure for too long names - result, err := headscale.Execute( + _, err = headscale.Execute( []string{ "headscale", "nodes", "rename", "--identifier", fmt.Sprintf("%d", listAll[4].GetId()), - "testmaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaachine12345678901234567890", + strings.Repeat("t", 64), }, ) - assert.Nil(t, err) - assert.Contains(t, result, "not be over 63 chars") + assert.ErrorContains(t, err, "not be over 63 chars") var listAllAfterRenameAttempt []v1.Node err = executeAndUnmarshal( @@ -1536,7 +1528,7 @@ func TestNodeMoveCommand(t *testing.T) { assert.Equal(t, allNodes[0].GetUser(), node.GetUser()) assert.Equal(t, allNodes[0].GetUser().GetName(), "new-user") - moveToNonExistingNSResult, err := headscale.Execute( + _, err = headscale.Execute( []string{ "headscale", "nodes", @@ -1549,11 +1541,9 @@ func TestNodeMoveCommand(t *testing.T) { "json", }, ) - assert.Nil(t, err) - - assert.Contains( + assert.ErrorContains( t, - moveToNonExistingNSResult, + err, "user not found", ) assert.Equal(t, node.GetUser().GetName(), "new-user") From 5597edac1ec70c1a623d6dd9c709b8ca97fb71a5 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 9 Sep 2024 08:57:50 +0200 Subject: [PATCH 36/60] Remove version and update setup instructions for Android (#2112) --- docs/android-client.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/android-client.md b/docs/android-client.md index 21dd8d21..044b9fcf 100644 --- a/docs/android-client.md +++ b/docs/android-client.md @@ -8,12 +8,9 @@ This documentation has the goal of showing how a user can use the official Andro Install the official Tailscale Android client from the [Google Play Store](https://play.google.com/store/apps/details?id=com.tailscale.ipn) or [F-Droid](https://f-droid.org/packages/com.tailscale.ipn/). -Ensure that the installed version is at least 1.30.0, as that is the first release to support custom URLs. - ## Configuring the headscale URL -After opening the app: - -- Open setting and go into account settings -- In the kebab menu icon (three dots) on the top bar on the right select “Use an alternate server” -- Enter your server URL and follow the instructions +- Open the app and select the settings menu in the upper-right corner +- Tap on `Accounts` +- In the kebab menu icon (three dots) in the upper-right corner select `Use an alternate server` +- Enter your server URL (e.g `https://headscale.example.com`) and follow the instructions From bac7ea67f4314870d1d8459c624c6ea10e352448 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 9 Sep 2024 13:18:16 +0200 Subject: [PATCH 37/60] Simplify windows setup instructions (#2114) * Simplify /windows to the bare minimum. Also remove the /windows/tailscale.reg endpoint as its generated file is no longer valid for current Tailscale versions. * Update and simplify the windows documentation accordingly. * Add a "Unattended mode" section to the troubleshooting section explaining how to enable "Unattended mode" in the via the Tailscale tray icon. * Add infobox about /windows to the docs Tested on Windows 10, 22H2 with Tailscale 1.72.0 Replaces: #1995 See: #2096 --- docs/images/windows-registry.png | Bin 103356 -> 0 bytes docs/windows-client.md | 61 ++++++++++++------------- hscontrol/app.go | 2 - hscontrol/platform_config.go | 52 ---------------------- hscontrol/templates/windows.html | 74 +++++-------------------------- mkdocs.yml | 2 +- 6 files changed, 42 insertions(+), 149 deletions(-) delete mode 100644 docs/images/windows-registry.png diff --git a/docs/images/windows-registry.png b/docs/images/windows-registry.png deleted file mode 100644 index 1324ca6c4d8e8e486d46569aabce5e2acd870ebc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103356 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¨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 W(P=HGmj~wn0000` as a `string` type, to ensure Tailscale contacts the correct control server. - -You can set these using the Windows Registry Editor: - -![windows-registry](./images/windows-registry.png) - -Or via the following Powershell commands (right click Powershell icon and select "Run as administrator"): - -``` -New-Item -Path "HKLM:\SOFTWARE\Tailscale IPN" -New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name UnattendedMode -PropertyType String -Value always -New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name LoginURL -PropertyType String -Value https://YOUR-HEADSCALE-URL -``` - -The Tailscale Windows client has been observed to reset its configuration on logout/reboot and these two keys [resolves that issue](https://github.com/tailscale/tailscale/issues/2798). - -For a guide on how to edit registry keys, [check out Computer Hope](https://www.computerhope.com/issues/ch001348.htm). - ## Installation Download the [Official Windows Client](https://tailscale.com/download/windows) and install it. -When the installation has finished, start Tailscale and log in (you might have to click the icon in the system tray). +## Configuring the headscale URL -The log in should open a browser Window and direct you to your `headscale` instance. +!!! info "Instructions on your headscale instance" + + An endpoint with information on how to connect your Windows device + is also available at `/windows` on your running instance. + +Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g +`https://headscale.example.com`): + +``` +tailscale login --login-server +``` + +Follow the instructions in the opened browser window to finish the configuration. ## Troubleshooting +### Unattended mode + +By default, Tailscale's Windows client is only running when the user is logged in. If you want to keep Tailscale running +all the time, please enable "Unattended mode": + +- Click on the Tailscale tray icon and select `Preferences` +- Enable `Run unattended` +- Confirm the "Unattended mode" message + +See also [Keep Tailscale running when I'm not logged in to my computer](https://tailscale.com/kb/1088/run-unattended) + +### Failing node registration + If you are seeing repeated messages like: ``` @@ -53,8 +55,7 @@ This typically means that the registry keys above was not set appropriately. To reset and try again, it is important to do the following: -1. Ensure the registry keys from the previous guide is correctly set. -2. Shut down the Tailscale service (or the client running in the tray) -3. Delete Tailscale Application data folder, located at `C:\Users\\AppData\Local\Tailscale` and try to connect again. -4. Ensure the Windows node is deleted from headscale (to ensure fresh setup) -5. Start Tailscale on the windows machine and retry the login. +1. Shut down the Tailscale service (or the client running in the tray) +2. Delete Tailscale Application data folder, located at `C:\Users\\AppData\Local\Tailscale` and try to connect again. +3. Ensure the Windows node is deleted from headscale (to ensure fresh setup) +4. Start Tailscale on the Windows machine and retry the login. diff --git a/hscontrol/app.go b/hscontrol/app.go index 087d2f2a..1732135a 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -437,8 +437,6 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { router.HandleFunc("/apple/{platform}", h.ApplePlatformConfig). Methods(http.MethodGet) router.HandleFunc("/windows", h.WindowsConfigMessage).Methods(http.MethodGet) - router.HandleFunc("/windows/tailscale.reg", h.WindowsRegConfig). - Methods(http.MethodGet) // TODO(kristoffer): move swagger into a package router.HandleFunc("/swagger", headscale.SwaggerUI).Methods(http.MethodGet) diff --git a/hscontrol/platform_config.go b/hscontrol/platform_config.go index 0404f546..9844a606 100644 --- a/hscontrol/platform_config.go +++ b/hscontrol/platform_config.go @@ -59,46 +59,6 @@ func (h *Headscale) WindowsConfigMessage( } } -// WindowsRegConfig generates and serves a .reg file configured with the Headscale server address. -func (h *Headscale) WindowsRegConfig( - writer http.ResponseWriter, - req *http.Request, -) { - config := WindowsRegistryConfig{ - URL: h.cfg.ServerURL, - } - - var content bytes.Buffer - if err := windowsRegTemplate.Execute(&content, config); err != nil { - log.Error(). - Str("handler", "WindowsRegConfig"). - Err(err). - Msg("Could not render Apple macOS template") - - writer.Header().Set("Content-Type", "text/plain; charset=utf-8") - writer.WriteHeader(http.StatusInternalServerError) - _, err := writer.Write([]byte("Could not render Windows registry template")) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } - - return - } - - writer.Header().Set("Content-Type", "text/x-ms-regedit; charset=utf-8") - writer.WriteHeader(http.StatusOK) - _, err := writer.Write(content.Bytes()) - if err != nil { - log.Error(). - Caller(). - Err(err). - Msg("Failed to write response") - } -} - // AppleConfigMessage shows a simple message in the browser to point the user to the iOS/MacOS profile and instructions for how to install it. func (h *Headscale) AppleConfigMessage( writer http.ResponseWriter, @@ -305,10 +265,6 @@ func (h *Headscale) ApplePlatformConfig( } } -type WindowsRegistryConfig struct { - URL string -} - type AppleMobileConfig struct { UUID uuid.UUID URL string @@ -320,14 +276,6 @@ type AppleMobilePlatformConfig struct { URL string } -var windowsRegTemplate = textTemplate.Must( - textTemplate.New("windowsconfig").Parse(`Windows Registry Editor Version 5.00 - -[HKEY_LOCAL_MACHINE\SOFTWARE\Tailscale IPN] -"UnattendedMode"="always" -"LoginURL"="{{.URL}}" -`)) - var commonTemplate = textTemplate.Must( textTemplate.New("mobileconfig").Parse(` diff --git a/hscontrol/templates/windows.html b/hscontrol/templates/windows.html index c590494f..34aaa0ae 100644 --- a/hscontrol/templates/windows.html +++ b/hscontrol/templates/windows.html @@ -25,75 +25,21 @@

headscale: Windows configuration

-

Recent Tailscale versions (1.34.0 and higher)

- Tailscale added Fast User Switching in version 1.34 and you can now use - the new login command to connect to one or more headscale (and Tailscale) - servers. The previously used profiles does not have an effect anymore. -

-

Use Tailscale's login command to add your profile:

-
tailscale login --login-server {{.URL}}
- -

Windows registry configuration (1.32.0 and lower)

-

- This page provides Windows registry information for the official Windows - Tailscale client. -

- -

-

- The registry file will configure Tailscale to use {{.URL}} as - its control server. -

- -

-

Caution

-

- You should always download and inspect the registry file before installing - it: -

-
curl {{.URL}}/windows/tailscale.reg
- -

Installation

-

- Headscale can be set to the default server by running the registry file: -

- -

- Windows registry fileTailscale for Windows + and install it.

-
    -
  1. Download the registry file, then run it
  2. -
  3. Follow the prompts
  4. -
  5. Install and run the official windows Tailscale client
  6. -
  7. - When the installation has finished, start Tailscale, and log in by - clicking the icon in the system tray -
  8. -
-

Or using REG:

- Open command prompt with Administrator rights. Issue the following - commands to add the required registry entries: + Open a Command Prompt or Powershell and use Tailscale's login command to + connect with headscale:

-
-    REG ADD "HKLM\Software\Tailscale IPN" /v UnattendedMode /t REG_SZ /d always
-      REG ADD "HKLM\Software\Tailscale IPN" /v LoginURL /t REG_SZ /d "{{.URL}}"
-  
-

Or using Powershell

-

- Open Powershell with Administrator rights. Issue the following commands to - add the required registry entries: -

-
-    New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name UnattendedMode -PropertyType String -Value always
-      New-ItemProperty -Path 'HKLM:\Software\Tailscale IPN' -Name LoginURL -PropertyType String -Value "{{.URL}}"
-  
-

Finally, restart Tailscale and log in.

- -

+
tailscale login --login-server {{.URL}}
diff --git a/mkdocs.yml b/mkdocs.yml index c14fd716..b88cfcc4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,7 +10,7 @@ repo_name: juanfont/headscale repo_url: https://github.com/juanfont/headscale # Copyright -copyright: Copyright © 2023 Headscale authors +copyright: Copyright © 2024 Headscale authors # Configuration theme: From 60b94b04675b41438ab679f3f2f4b0a0310179a4 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 9 Sep 2024 14:10:22 +0200 Subject: [PATCH 38/60] Fix slow shutdown (#2113) * rearrange shutdown Signed-off-by: Kristoffer Dalby * http closed is fine Signed-off-by: Kristoffer Dalby * update changelog Signed-off-by: Kristoffer Dalby * logging while shutting Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 1 + cmd/headscale/cli/serve.go | 7 +++++-- hscontrol/app.go | 36 +++++++++++++++++----------------- hscontrol/notifier/notifier.go | 34 +++++++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91aed9ef..d9818217 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -74,6 +74,7 @@ after improving the test harness as part of adopting [#1460](https://github.com/ - Make sure integration tests cover postgres for all scenarios - CLI commands (all except `serve`) only requires minimal configuration, no more errors or warnings from unset settings [#2109](https://github.com/juanfont/headscale/pull/2109) - CLI results are now concistently sent to stdout and errors to stderr [#2109](https://github.com/juanfont/headscale/pull/2109) +- Fix issue where shutting down headscale would hang [#2113](https://github.com/juanfont/headscale/pull/2113) ## 0.22.3 (2023-05-12) diff --git a/cmd/headscale/cli/serve.go b/cmd/headscale/cli/serve.go index 9f0fa35e..91597400 100644 --- a/cmd/headscale/cli/serve.go +++ b/cmd/headscale/cli/serve.go @@ -1,6 +1,9 @@ package cli import ( + "errors" + "net/http" + "github.com/rs/zerolog/log" "github.com/spf13/cobra" ) @@ -22,8 +25,8 @@ var serveCmd = &cobra.Command{ } err = app.Serve() - if err != nil { - log.Fatal().Caller().Err(err).Msg("Error starting server") + if err != nil && !errors.Is(err, http.ErrServerClosed) { + log.Fatal().Caller().Err(err).Msg("Headscale ran into an error and had to shut down.") } }, } diff --git a/hscontrol/app.go b/hscontrol/app.go index 1732135a..4a5b4679 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -770,7 +770,7 @@ func (h *Headscale) Serve() error { }) } default: - trace := log.Trace().Msgf + info := func(msg string) { log.Info().Msg(msg) } log.Info(). Str("signal", sig.String()). Msg("Received signal to stop, shutting down gracefully") @@ -778,55 +778,55 @@ func (h *Headscale) Serve() error { expireNodeCancel() h.ephemeralGC.Close() - trace("waiting for netmap stream to close") - h.pollNetMapStreamWG.Wait() - // Gracefully shut down servers ctx, cancel := context.WithTimeout( context.Background(), types.HTTPShutdownTimeout, ) - trace("shutting down debug http server") + info("shutting down debug http server") if err := debugHTTPServer.Shutdown(ctx); err != nil { - log.Error().Err(err).Msg("Failed to shutdown prometheus http") + log.Error().Err(err).Msg("failed to shutdown prometheus http") } - trace("shutting down main http server") + info("shutting down main http server") if err := httpServer.Shutdown(ctx); err != nil { - log.Error().Err(err).Msg("Failed to shutdown http") + log.Error().Err(err).Msg("failed to shutdown http") } - trace("shutting down grpc server (socket)") + info("closing node notifier") + h.nodeNotifier.Close() + + info("waiting for netmap stream to close") + h.pollNetMapStreamWG.Wait() + + info("shutting down grpc server (socket)") grpcSocket.GracefulStop() if grpcServer != nil { - trace("shutting down grpc server (external)") + info("shutting down grpc server (external)") grpcServer.GracefulStop() grpcListener.Close() } if tailsqlContext != nil { - trace("shutting down tailsql") + info("shutting down tailsql") tailsqlContext.Done() } - trace("closing node notifier") - h.nodeNotifier.Close() - // Close network listeners - trace("closing network listeners") + info("closing network listeners") debugHTTPListener.Close() httpListener.Close() grpcGatewayConn.Close() // Stop listening (and unlink the socket if unix type): - trace("closing socket listener") + info("closing socket listener") socketListener.Close() // Close db connections - trace("closing database connection") + info("closing database connection") err = h.db.Close() if err != nil { - log.Error().Err(err).Msg("Failed to close db") + log.Error().Err(err).Msg("failed to close db") } log.Info(). diff --git a/hscontrol/notifier/notifier.go b/hscontrol/notifier/notifier.go index 0b663776..ceede6ba 100644 --- a/hscontrol/notifier/notifier.go +++ b/hscontrol/notifier/notifier.go @@ -36,6 +36,7 @@ type Notifier struct { connected *xsync.MapOf[types.NodeID, bool] b *batcher cfg *types.Config + closed bool } func NewNotifier(cfg *types.Config) *Notifier { @@ -43,6 +44,7 @@ func NewNotifier(cfg *types.Config) *Notifier { nodes: make(map[types.NodeID]chan<- types.StateUpdate), connected: xsync.NewMapOf[types.NodeID, bool](), cfg: cfg, + closed: false, } b := newBatcher(cfg.Tuning.BatchChangeDelay, n) n.b = b @@ -51,9 +53,19 @@ func NewNotifier(cfg *types.Config) *Notifier { return n } -// Close stops the batcher inside the notifier. +// Close stops the batcher and closes all channels. func (n *Notifier) Close() { + notifierWaitersForLock.WithLabelValues("lock", "close").Inc() + n.l.Lock() + defer n.l.Unlock() + notifierWaitersForLock.WithLabelValues("lock", "close").Dec() + + n.closed = true n.b.close() + + for _, c := range n.nodes { + close(c) + } } func (n *Notifier) tracef(nID types.NodeID, msg string, args ...any) { @@ -70,6 +82,10 @@ func (n *Notifier) AddNode(nodeID types.NodeID, c chan<- types.StateUpdate) { notifierWaitersForLock.WithLabelValues("lock", "add").Dec() notifierWaitForLock.WithLabelValues("add").Observe(time.Since(start).Seconds()) + if n.closed { + return + } + // If a channel exists, it means the node has opened a new // connection. Close the old channel and replace it. if curr, ok := n.nodes[nodeID]; ok { @@ -96,6 +112,10 @@ func (n *Notifier) RemoveNode(nodeID types.NodeID, c chan<- types.StateUpdate) b notifierWaitersForLock.WithLabelValues("lock", "remove").Dec() notifierWaitForLock.WithLabelValues("remove").Observe(time.Since(start).Seconds()) + if n.closed { + return true + } + if len(n.nodes) == 0 { return true } @@ -154,6 +174,10 @@ func (n *Notifier) NotifyWithIgnore( update types.StateUpdate, ignoreNodeIDs ...types.NodeID, ) { + if n.closed { + return + } + notifierUpdateReceived.WithLabelValues(update.Type.String(), types.NotifyOriginKey.Value(ctx)).Inc() n.b.addOrPassthrough(update) } @@ -170,6 +194,10 @@ func (n *Notifier) NotifyByNodeID( notifierWaitersForLock.WithLabelValues("lock", "notify").Dec() notifierWaitForLock.WithLabelValues("notify").Observe(time.Since(start).Seconds()) + if n.closed { + return + } + if c, ok := n.nodes[nodeID]; ok { select { case <-ctx.Done(): @@ -205,6 +233,10 @@ func (n *Notifier) sendAll(update types.StateUpdate) { notifierWaitersForLock.WithLabelValues("lock", "send-all").Dec() notifierWaitForLock.WithLabelValues("send-all").Observe(time.Since(start).Seconds()) + if n.closed { + return + } + for id, c := range n.nodes { // Whenever an update is sent to all nodes, there is a chance that the node // has disconnected and the goroutine that was supposed to consume the update From c3b260a6f7190105e64e48cacd85db3f7d53317c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 14:16:35 +0200 Subject: [PATCH 39/60] flake.lock: Update (#2111) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index cd36fb42..9b66e4e0 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1725099143, - "narHash": "sha256-CHgumPZaC7z+WYx72WgaLt2XF0yUVzJS60rO4GZ7ytY=", + "lastModified": 1725534445, + "narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5629520edecb69630a3f4d17d3d33fc96c13f6fe", + "rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39", "type": "github" }, "original": { From 99f18f9cd90c5b806d390473c3aaa89a5aca3ad2 Mon Sep 17 00:00:00 2001 From: curlwget Date: Mon, 9 Sep 2024 20:17:25 +0800 Subject: [PATCH 40/60] chore: fix some comments (#2069) --- hscontrol/mapper/mapper.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/hscontrol/mapper/mapper.go b/hscontrol/mapper/mapper.go index 702b7845..8593e167 100644 --- a/hscontrol/mapper/mapper.go +++ b/hscontrol/mapper/mapper.go @@ -227,7 +227,7 @@ func (m *Mapper) FullMapResponse( return m.marshalMapResponse(mapRequest, resp, node, mapRequest.Compress, messages...) } -// ReadOnlyResponse returns a MapResponse for the given node. +// ReadOnlyMapResponse returns a MapResponse for the given node. // Lite means that the peers has been omitted, this is intended // to be used to answer MapRequests with OmitPeers set to true. func (m *Mapper) ReadOnlyMapResponse( @@ -552,7 +552,7 @@ func appendPeerChanges( } // If there are filter rules present, see if there are any nodes that cannot - // access eachother at all and remove them from the peers. + // access each-other at all and remove them from the peers. if len(packetFilter) > 0 { changed = policy.FilterNodesByACL(node, changed, packetFilter) } @@ -596,7 +596,7 @@ func appendPeerChanges( } else { // This is a hack to avoid sending an empty list of packet filters. // Since tailcfg.PacketFilter has omitempty, any empty PacketFilter will - // be omitted, causing the client to consider it unchange, keeping the + // be omitted, causing the client to consider it unchanged, keeping the // previous packet filter. Worst case, this can cause a node that previously // has access to a node to _not_ loose access if an empty (allow none) is sent. reduced := policy.ReduceFilterRules(node, packetFilter) From 7be8796d87d2f65cdac200e3fa26febe1260bf72 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Mon, 9 Sep 2024 14:29:09 +0200 Subject: [PATCH 41/60] dont override golangci go (#2116) Signed-off-by: Kristoffer Dalby --- flake.nix | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/flake.nix b/flake.nix index 8e009c1f..79dd58e8 100644 --- a/flake.nix +++ b/flake.nix @@ -57,9 +57,11 @@ subPackages = ["protoc-gen-grpc-gateway" "protoc-gen-openapiv2"]; }; - golangci-lint = prev.golangci-lint.override { - buildGoModule = buildGo; - }; + # Upstream does not override buildGoModule properly, + # importing a specific module, so comment out for now. + # golangci-lint = prev.golangci-lint.override { + # buildGoModule = buildGo; + # }; goreleaser = prev.goreleaser.override { buildGoModule = buildGo; From 4b02dc95653f8c24be1effa8c94e9b3646595b68 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Sep 2024 10:43:22 +0200 Subject: [PATCH 42/60] make cli mode respect log.level (#2124) Fixes #2119 Signed-off-by: Kristoffer Dalby --- hscontrol/types/config.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go index 8767077e..50ce2f07 100644 --- a/hscontrol/types/config.go +++ b/hscontrol/types/config.go @@ -732,6 +732,9 @@ func prefixV6() (*netip.Prefix, error) { // LoadCLIConfig returns the needed configuration for the CLI client // of Headscale to connect to a Headscale server. func LoadCLIConfig() (*Config, error) { + logConfig := logConfig() + zerolog.SetGlobalLevel(logConfig.Level) + return &Config{ DisableUpdateCheck: viper.GetBool("disable_check_updates"), UnixSocket: viper.GetString("unix_socket"), @@ -741,6 +744,7 @@ func LoadCLIConfig() (*Config, error) { Timeout: viper.GetDuration("cli.timeout"), Insecure: viper.GetBool("cli.insecure"), }, + Log: logConfig, }, nil } From 64319f79ff1934865805fc73be2228dddce0ec80 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Sep 2024 12:00:32 +0200 Subject: [PATCH 43/60] make stream shutdown if self-node has been removed (#2125) * add shutdown that asserts if headscale had panics Signed-off-by: Kristoffer Dalby * add test case producing 2118 panic Signed-off-by: Kristoffer Dalby * make stream shutdown if self-node has been removed Currently we will read the node from database, and since it is deleted, the id might be set to nil. Keep the node around and just shutdown, so it is cleanly removed from notifier. Fixes #2118 Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- .github/workflows/test-integration.yaml | 1 + hscontrol/poll.go | 7 ++ integration/control.go | 4 +- integration/dockertestutil/logs.go | 18 +++-- integration/general_test.go | 99 +++++++++++++++++++++++++ integration/hsic/hsic.go | 8 +- integration/scenario.go | 28 +++++-- integration/tsic/tsic.go | 4 +- 8 files changed, 148 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index ed194da1..d6c7eff2 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -52,6 +52,7 @@ jobs: - TestExpireNode - TestNodeOnlineStatus - TestPingAllByIPManyUpDown + - Test2118DeletingOnlineNodePanics - TestEnablingRoutes - TestHASubnetRouterFailover - TestEnableDisableAutoApprovedRoute diff --git a/hscontrol/poll.go b/hscontrol/poll.go index d7ba682e..82a5295f 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand/v2" "net/http" + "slices" "sort" "strings" "time" @@ -273,6 +274,12 @@ func (m *mapSession) serveLongPoll() { return } + // If the node has been removed from headscale, close the stream + if slices.Contains(update.Removed, m.node.ID) { + m.tracef("node removed, closing stream") + return + } + m.tracef("received stream update: %s %s", update.Type.String(), update.Message) mapResponseUpdateReceived.WithLabelValues(update.Type.String()).Inc() diff --git a/integration/control.go b/integration/control.go index 8a34bde8..b5699577 100644 --- a/integration/control.go +++ b/integration/control.go @@ -6,8 +6,8 @@ import ( ) type ControlServer interface { - Shutdown() error - SaveLog(string) error + Shutdown() (string, string, error) + SaveLog(string) (string, string, error) SaveProfile(string) error Execute(command []string) (string, error) WriteFile(path string, content []byte) error diff --git a/integration/dockertestutil/logs.go b/integration/dockertestutil/logs.go index 98ba970a..64c3c9ac 100644 --- a/integration/dockertestutil/logs.go +++ b/integration/dockertestutil/logs.go @@ -17,10 +17,10 @@ func SaveLog( pool *dockertest.Pool, resource *dockertest.Resource, basePath string, -) error { +) (string, string, error) { err := os.MkdirAll(basePath, os.ModePerm) if err != nil { - return err + return "", "", err } var stdout bytes.Buffer @@ -41,28 +41,30 @@ func SaveLog( }, ) if err != nil { - return err + return "", "", err } log.Printf("Saving logs for %s to %s\n", resource.Container.Name, basePath) + stdoutPath := path.Join(basePath, resource.Container.Name+".stdout.log") err = os.WriteFile( - path.Join(basePath, resource.Container.Name+".stdout.log"), + stdoutPath, stdout.Bytes(), filePerm, ) if err != nil { - return err + return "", "", err } + stderrPath := path.Join(basePath, resource.Container.Name+".stderr.log") err = os.WriteFile( - path.Join(basePath, resource.Container.Name+".stderr.log"), + stderrPath, stderr.Bytes(), filePerm, ) if err != nil { - return err + return "", "", err } - return nil + return stdoutPath, stderrPath, nil } diff --git a/integration/general_test.go b/integration/general_test.go index 6de00fd2..a8421f47 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -954,3 +954,102 @@ func TestPingAllByIPManyUpDown(t *testing.T) { t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) } } + +func Test2118DeletingOnlineNodePanics(t *testing.T) { + IntegrationSkip(t) + t.Parallel() + + scenario, err := NewScenario(dockertestMaxWait()) + assertNoErr(t, err) + defer scenario.ShutdownAssertNoPanics(t) + + // TODO(kradalby): it does not look like the user thing works, only second + // get created? maybe only when many? + spec := map[string]int{ + "user1": 1, + "user2": 1, + } + + err = scenario.CreateHeadscaleEnv(spec, + []tsic.Option{}, + hsic.WithTestName("deletenocrash"), + hsic.WithEmbeddedDERPServerOnly(), + hsic.WithTLS(), + hsic.WithHostnameAsServerURL(), + ) + assertNoErrHeadscaleEnv(t, err) + + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + + allIps, err := scenario.ListTailscaleClientsIPs() + assertNoErrListClientIPs(t, err) + + err = scenario.WaitForTailscaleSync() + assertNoErrSync(t, err) + + allAddrs := lo.Map(allIps, func(x netip.Addr, index int) string { + return x.String() + }) + + success := pingAllHelper(t, allClients, allAddrs) + t.Logf("%d successful pings out of %d", success, len(allClients)*len(allIps)) + + headscale, err := scenario.Headscale() + assertNoErr(t, err) + + // Test list all nodes after added otherUser + var nodeList []v1.Node + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &nodeList, + ) + assert.Nil(t, err) + assert.Len(t, nodeList, 2) + assert.True(t, nodeList[0].Online) + assert.True(t, nodeList[1].Online) + + // Delete the first node, which is online + _, err = headscale.Execute( + []string{ + "headscale", + "nodes", + "delete", + "--identifier", + // Delete the last added machine + fmt.Sprintf("%d", nodeList[0].Id), + "--output", + "json", + "--force", + }, + ) + assert.Nil(t, err) + + time.Sleep(2 * time.Second) + + // Ensure that the node has been deleted, this did not occur due to a panic. + var nodeListAfter []v1.Node + err = executeAndUnmarshal( + headscale, + []string{ + "headscale", + "nodes", + "list", + "--output", + "json", + }, + &nodeListAfter, + ) + assert.Nil(t, err) + assert.Len(t, nodeListAfter, 1) + assert.True(t, nodeListAfter[0].Online) + assert.Equal(t, nodeList[1].Id, nodeListAfter[0].Id) + +} diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index b9026225..20a778b8 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -398,8 +398,8 @@ func (t *HeadscaleInContainer) hasTLS() bool { } // Shutdown stops and cleans up the Headscale container. -func (t *HeadscaleInContainer) Shutdown() error { - err := t.SaveLog("/tmp/control") +func (t *HeadscaleInContainer) Shutdown() (string, string, error) { + stdoutPath, stderrPath, err := t.SaveLog("/tmp/control") if err != nil { log.Printf( "Failed to save log from control: %s", @@ -458,12 +458,12 @@ func (t *HeadscaleInContainer) Shutdown() error { t.pool.Purge(t.pgContainer) } - return t.pool.Purge(t.container) + return stdoutPath, stderrPath, t.pool.Purge(t.container) } // SaveLog saves the current stdout log of the container to a path // on the host system. -func (t *HeadscaleInContainer) SaveLog(path string) error { +func (t *HeadscaleInContainer) SaveLog(path string) (string, string, error) { return dockertestutil.SaveLog(t.pool, t.container, path) } diff --git a/integration/scenario.go b/integration/scenario.go index 075d1fd5..df978f2a 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -8,6 +8,7 @@ import ( "os" "sort" "sync" + "testing" "time" v1 "github.com/juanfont/headscale/gen/go/headscale/v1" @@ -18,6 +19,7 @@ import ( "github.com/ory/dockertest/v3" "github.com/puzpuzpuz/xsync/v3" "github.com/samber/lo" + "github.com/stretchr/testify/assert" "golang.org/x/sync/errgroup" "tailscale.com/envknob" ) @@ -187,13 +189,9 @@ func NewScenario(maxWait time.Duration) (*Scenario, error) { }, nil } -// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient) -// and networks associated with it. -// In addition, it will save the logs of the ControlServer to `/tmp/control` in the -// environment running the tests. -func (s *Scenario) Shutdown() { +func (s *Scenario) ShutdownAssertNoPanics(t *testing.T) { s.controlServers.Range(func(_ string, control ControlServer) bool { - err := control.Shutdown() + stdoutPath, stderrPath, err := control.Shutdown() if err != nil { log.Printf( "Failed to shut down control: %s", @@ -201,6 +199,16 @@ func (s *Scenario) Shutdown() { ) } + if t != nil { + stdout, err := os.ReadFile(stdoutPath) + assert.NoError(t, err) + assert.NotContains(t, string(stdout), "panic") + + stderr, err := os.ReadFile(stderrPath) + assert.NoError(t, err) + assert.NotContains(t, string(stderr), "panic") + } + return true }) @@ -224,6 +232,14 @@ func (s *Scenario) Shutdown() { // } } +// Shutdown shuts down and cleans up all the containers (ControlServer, TailscaleClient) +// and networks associated with it. +// In addition, it will save the logs of the ControlServer to `/tmp/control` in the +// environment running the tests. +func (s *Scenario) Shutdown() { + s.ShutdownAssertNoPanics(nil) +} + // Users returns the name of all users associated with the Scenario. func (s *Scenario) Users() []string { users := make([]string, 0) diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index e1045ec3..a3fac17c 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -998,7 +998,9 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { // SaveLog saves the current stdout log of the container to a path // on the host system. func (t *TailscaleInContainer) SaveLog(path string) error { - return dockertestutil.SaveLog(t.pool, t.container, path) + // TODO(kradalby): Assert if tailscale logs contains panics. + _, _, err := dockertestutil.SaveLog(t.pool, t.container, path) + return err } // ReadFile reads a file from the Tailscale container. From 064c46f2a5889a328627673f153a01c26812c945 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Sep 2024 18:27:49 +0200 Subject: [PATCH 44/60] move logic for validating node names (#2127) * move logic for validating node names this commits moves the generation of "given names" of nodes into the registration function, and adds validation of renames to RenameNode using the same logic. Fixes #2121 Signed-off-by: Kristoffer Dalby * fix double arg Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- hscontrol/auth.go | 26 +------ hscontrol/db/node.go | 72 +++++++++---------- hscontrol/db/node_test.go | 143 ++++++++++++++++++++++++++------------ hscontrol/grpcv1.go | 8 +-- 4 files changed, 134 insertions(+), 115 deletions(-) diff --git a/hscontrol/auth.go b/hscontrol/auth.go index aaab03ce..8b8557ba 100644 --- a/hscontrol/auth.go +++ b/hscontrol/auth.go @@ -66,7 +66,7 @@ func (h *Headscale) handleRegister( regReq tailcfg.RegisterRequest, machineKey key.MachinePublic, ) { - logInfo, logTrace, logErr := logAuthFunc(regReq, machineKey) + logInfo, logTrace, _ := logAuthFunc(regReq, machineKey) now := time.Now().UTC() logTrace("handleRegister called, looking up machine in DB") node, err := h.db.GetNodeByAnyKey(machineKey, regReq.NodeKey, regReq.OldNodeKey) @@ -105,16 +105,6 @@ func (h *Headscale) handleRegister( logInfo("Node not found in database, creating new") - givenName, err := h.db.GenerateGivenName( - machineKey, - regReq.Hostinfo.Hostname, - ) - if err != nil { - logErr(err, "Failed to generate given name for node") - - return - } - // The node did not have a key to authenticate, which means // that we rely on a method that calls back some how (OpenID or CLI) // We create the node and then keep it around until a callback @@ -122,7 +112,6 @@ func (h *Headscale) handleRegister( newNode := types.Node{ MachineKey: machineKey, Hostname: regReq.Hostinfo.Hostname, - GivenName: givenName, NodeKey: regReq.NodeKey, LastSeen: &now, Expiry: &time.Time{}, @@ -354,21 +343,8 @@ func (h *Headscale) handleAuthKey( } else { now := time.Now().UTC() - givenName, err := h.db.GenerateGivenName(machineKey, registerRequest.Hostinfo.Hostname) - if err != nil { - log.Error(). - Caller(). - Str("func", "RegistrationHandler"). - Str("hostinfo.name", registerRequest.Hostinfo.Hostname). - Err(err). - Msg("Failed to generate given name for node") - - return - } - nodeToRegister := types.Node{ Hostname: registerRequest.Hostinfo.Hostname, - GivenName: givenName, UserID: pak.User.ID, User: pak.User, MachineKey: machineKey, diff --git a/hscontrol/db/node.go b/hscontrol/db/node.go index a9e78a45..c0f42de1 100644 --- a/hscontrol/db/node.go +++ b/hscontrol/db/node.go @@ -90,20 +90,6 @@ func (hsdb *HSDatabase) ListEphemeralNodes() (types.Nodes, error) { }) } -func listNodesByGivenName(tx *gorm.DB, givenName string) (types.Nodes, error) { - nodes := types.Nodes{} - if err := tx. - Preload("AuthKey"). - Preload("AuthKey.User"). - Preload("User"). - Preload("Routes"). - Where("given_name = ?", givenName).Find(&nodes).Error; err != nil { - return nil, err - } - - return nodes, nil -} - func (hsdb *HSDatabase) getNode(user string, name string) (*types.Node, error) { return Read(hsdb.DB, func(rx *gorm.DB) (*types.Node, error) { return getNode(rx, user, name) @@ -242,9 +228,9 @@ func SetTags( } // RenameNode takes a Node struct and a new GivenName for the nodes -// and renames it. +// and renames it. If the name is not unique, it will return an error. func RenameNode(tx *gorm.DB, - nodeID uint64, newName string, + nodeID types.NodeID, newName string, ) error { err := util.CheckForFQDNRules( newName, @@ -253,6 +239,15 @@ func RenameNode(tx *gorm.DB, return fmt.Errorf("renaming node: %w", err) } + uniq, err := isUnqiueName(tx, newName) + if err != nil { + return fmt.Errorf("checking if name is unique: %w", err) + } + + if !uniq { + return fmt.Errorf("name is not unique: %s", newName) + } + if err := tx.Model(&types.Node{}).Where("id = ?", nodeID).Update("given_name", newName).Error; err != nil { return fmt.Errorf("failed to rename node in the database: %w", err) } @@ -415,6 +410,15 @@ func RegisterNode(tx *gorm.DB, node types.Node, ipv4 *netip.Addr, ipv6 *netip.Ad node.IPv4 = ipv4 node.IPv6 = ipv6 + if node.GivenName == "" { + givenName, err := ensureUniqueGivenName(tx, node.Hostname) + if err != nil { + return nil, fmt.Errorf("failed to ensure unique given name: %w", err) + } + + node.GivenName = givenName + } + if err := tx.Save(&node).Error; err != nil { return nil, fmt.Errorf("failed register(save) node in the database: %w", err) } @@ -642,40 +646,32 @@ func generateGivenName(suppliedName string, randomSuffix bool) (string, error) { return normalizedHostname, nil } -func (hsdb *HSDatabase) GenerateGivenName( - mkey key.MachinePublic, - suppliedName string, -) (string, error) { - return Read(hsdb.DB, func(rx *gorm.DB) (string, error) { - return GenerateGivenName(rx, mkey, suppliedName) - }) +func isUnqiueName(tx *gorm.DB, name string) (bool, error) { + nodes := types.Nodes{} + if err := tx. + Where("given_name = ?", name).Find(&nodes).Error; err != nil { + return false, err + } + + return len(nodes) == 0, nil } -func GenerateGivenName( +func ensureUniqueGivenName( tx *gorm.DB, - mkey key.MachinePublic, - suppliedName string, + name string, ) (string, error) { - givenName, err := generateGivenName(suppliedName, false) + givenName, err := generateGivenName(name, false) if err != nil { return "", err } - // Tailscale rules (may differ) https://tailscale.com/kb/1098/machine-names/ - nodes, err := listNodesByGivenName(tx, givenName) + unique, err := isUnqiueName(tx, givenName) if err != nil { return "", err } - var nodeFound *types.Node - for idx, node := range nodes { - if node.GivenName == givenName { - nodeFound = nodes[idx] - } - } - - if nodeFound != nil && nodeFound.MachineKey.String() != mkey.String() { - postfixedName, err := generateGivenName(suppliedName, true) + if !unique { + postfixedName, err := generateGivenName(name, true) if err != nil { return "", err } diff --git a/hscontrol/db/node_test.go b/hscontrol/db/node_test.go index 94cce13b..bafb22ba 100644 --- a/hscontrol/db/node_test.go +++ b/hscontrol/db/node_test.go @@ -19,6 +19,7 @@ import ( "github.com/puzpuzpuz/xsync/v3" "github.com/stretchr/testify/assert" "gopkg.in/check.v1" + "gorm.io/gorm" "tailscale.com/tailcfg" "tailscale.com/types/key" "tailscale.com/types/ptr" @@ -313,51 +314,6 @@ func (s *Suite) TestExpireNode(c *check.C) { c.Assert(nodeFromDB.IsExpired(), check.Equals, true) } -func (s *Suite) TestGenerateGivenName(c *check.C) { - user1, err := db.CreateUser("user-1") - c.Assert(err, check.IsNil) - - pak, err := db.CreatePreAuthKey(user1.Name, false, false, nil, nil) - c.Assert(err, check.IsNil) - - _, err = db.getNode("user-1", "testnode") - c.Assert(err, check.NotNil) - - nodeKey := key.NewNode() - machineKey := key.NewMachine() - - machineKey2 := key.NewMachine() - - node := &types.Node{ - ID: 0, - MachineKey: machineKey.Public(), - NodeKey: nodeKey.Public(), - Hostname: "hostname-1", - GivenName: "hostname-1", - UserID: user1.ID, - RegisterMethod: util.RegisterMethodAuthKey, - AuthKeyID: ptr.To(pak.ID), - } - - trx := db.DB.Save(node) - c.Assert(trx.Error, check.IsNil) - - givenName, err := db.GenerateGivenName(machineKey2.Public(), "hostname-2") - comment := check.Commentf("Same user, unique nodes, unique hostnames, no conflict") - c.Assert(err, check.IsNil, comment) - c.Assert(givenName, check.Equals, "hostname-2", comment) - - givenName, err = db.GenerateGivenName(machineKey.Public(), "hostname-1") - comment = check.Commentf("Same user, same node, same hostname, no conflict") - c.Assert(err, check.IsNil, comment) - c.Assert(givenName, check.Equals, "hostname-1", comment) - - givenName, err = db.GenerateGivenName(machineKey2.Public(), "hostname-1") - comment = check.Commentf("Same user, unique nodes, same hostname, conflict") - c.Assert(err, check.IsNil, comment) - c.Assert(givenName, check.Matches, fmt.Sprintf("^hostname-1-[a-z0-9]{%d}$", NodeGivenNameHashLength), comment) -} - func (s *Suite) TestSetTags(c *check.C) { user, err := db.CreateUser("test") c.Assert(err, check.IsNil) @@ -778,3 +734,100 @@ func TestListEphemeralNodes(t *testing.T) { assert.Equal(t, nodeEph.UserID, ephemeralNodes[0].UserID) assert.Equal(t, nodeEph.Hostname, ephemeralNodes[0].Hostname) } + +func TestRenameNode(t *testing.T) { + db, err := newTestDB() + if err != nil { + t.Fatalf("creating db: %s", err) + } + + user, err := db.CreateUser("test") + assert.NoError(t, err) + + user2, err := db.CreateUser("test2") + assert.NoError(t, err) + + node := types.Node{ + ID: 0, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + Hostname: "test", + UserID: user.ID, + RegisterMethod: util.RegisterMethodAuthKey, + } + + node2 := types.Node{ + ID: 0, + MachineKey: key.NewMachine().Public(), + NodeKey: key.NewNode().Public(), + Hostname: "test", + UserID: user2.ID, + RegisterMethod: util.RegisterMethodAuthKey, + } + + err = db.DB.Save(&node).Error + assert.NoError(t, err) + + err = db.DB.Save(&node2).Error + assert.NoError(t, err) + + err = db.DB.Transaction(func(tx *gorm.DB) error { + _, err := RegisterNode(tx, node, nil, nil) + if err != nil { + return err + } + _, err = RegisterNode(tx, node2, nil, nil) + return err + }) + assert.NoError(t, err) + + nodes, err := db.ListNodes() + assert.NoError(t, err) + + assert.Len(t, nodes, 2) + + t.Logf("node1 %s %s", nodes[0].Hostname, nodes[0].GivenName) + t.Logf("node2 %s %s", nodes[1].Hostname, nodes[1].GivenName) + + assert.Equal(t, nodes[0].Hostname, nodes[0].GivenName) + assert.NotEqual(t, nodes[1].Hostname, nodes[1].GivenName) + assert.Equal(t, nodes[0].Hostname, nodes[1].Hostname) + assert.NotEqual(t, nodes[0].Hostname, nodes[1].GivenName) + assert.Contains(t, nodes[1].GivenName, nodes[0].Hostname) + assert.Equal(t, nodes[0].GivenName, nodes[1].Hostname) + assert.Len(t, nodes[0].Hostname, 4) + assert.Len(t, nodes[1].Hostname, 4) + assert.Len(t, nodes[0].GivenName, 4) + assert.Len(t, nodes[1].GivenName, 13) + + // Nodes can be renamed to a unique name + err = db.Write(func(tx *gorm.DB) error { + return RenameNode(tx, nodes[0].ID, "newname") + }) + assert.NoError(t, err) + + nodes, err = db.ListNodes() + assert.NoError(t, err) + assert.Len(t, nodes, 2) + assert.Equal(t, nodes[0].Hostname, "test") + assert.Equal(t, nodes[0].GivenName, "newname") + + // Nodes can reuse name that is no longer used + err = db.Write(func(tx *gorm.DB) error { + return RenameNode(tx, nodes[1].ID, "test") + }) + assert.NoError(t, err) + + nodes, err = db.ListNodes() + assert.NoError(t, err) + assert.Len(t, nodes, 2) + assert.Equal(t, nodes[0].Hostname, "test") + assert.Equal(t, nodes[0].GivenName, "newname") + assert.Equal(t, nodes[1].GivenName, "test") + + // Nodes cannot be renamed to used names + err = db.Write(func(tx *gorm.DB) error { + return RenameNode(tx, nodes[0].ID, "test") + }) + assert.ErrorContains(t, err, "name is not unique") +} diff --git a/hscontrol/grpcv1.go b/hscontrol/grpcv1.go index 3f985d98..596748f2 100644 --- a/hscontrol/grpcv1.go +++ b/hscontrol/grpcv1.go @@ -373,7 +373,7 @@ func (api headscaleV1APIServer) RenameNode( node, err := db.Write(api.h.db.DB, func(tx *gorm.DB) (*types.Node, error) { err := db.RenameNode( tx, - request.GetNodeId(), + types.NodeID(request.GetNodeId()), request.GetNewName(), ) if err != nil { @@ -802,18 +802,12 @@ func (api headscaleV1APIServer) DebugCreateNode( return nil, err } - givenName, err := api.h.db.GenerateGivenName(mkey, request.GetName()) - if err != nil { - return nil, err - } - nodeKey := key.NewNode() newNode := types.Node{ MachineKey: mkey, NodeKey: nodeKey.Public(), Hostname: request.GetName(), - GivenName: givenName, User: *user, Expiry: &time.Time{}, From c3ef90a7f7b9e742ce55100db8a9af44f8540c7d Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 11 Sep 2024 18:43:59 +0200 Subject: [PATCH 45/60] Update documentation for Apple (#2117) * Rename docs/ios-client.md to docs/apple-client.md. Add instructions for macOS; those are copied from the /apple endpoint and slightly modified. Fix doc links in the README. * Move infoboxes for /apple and /windows under the "Goal" section to the top. Those should be seen by users first as they contain *their* specific headscale URL. * Swap order of macOS and iOS to move "Profiles" further down. * Remove apple configuration profiles * Remove Tailscale versions hints * Mention /apple and /windows in the README along with their docs See: #2096 --- README.md | 18 ++--- docs/apple-client.md | 51 +++++++++++++ docs/iOS-client.md | 30 -------- docs/windows-client.md | 10 +-- hscontrol/templates/apple.html | 129 ++++++++++++--------------------- integration/scenario_test.go | 2 +- mkdocs.yml | 2 +- 7 files changed, 112 insertions(+), 130 deletions(-) create mode 100644 docs/apple-client.md delete mode 100644 docs/iOS-client.md diff --git a/README.md b/README.md index 03802e27..ff44e8e4 100644 --- a/README.md +++ b/README.md @@ -62,15 +62,15 @@ buttons available in the repo. ## Client OS support -| OS | Supports headscale | -| ------- | --------------------------------------------------------- | -| Linux | Yes | -| OpenBSD | Yes | -| FreeBSD | Yes | -| macOS | Yes (see `/apple` on your headscale for more information) | -| Windows | Yes [docs](./docs/windows-client.md) | -| Android | Yes [docs](./docs/android-client.md) | -| iOS | Yes [docs](./docs/iOS-client.md) | +| OS | Supports headscale | +| ------- | -------------------------------------------------------------------------------------------------- | +| Linux | Yes | +| OpenBSD | Yes | +| FreeBSD | Yes | +| Windows | Yes (see [docs](./docs/windows-client.md) and `/windows` on your headscale for more information) | +| Android | Yes (see [docs](./docs/android-client.md)) | +| macOS | Yes (see [docs](./docs/apple-client.md#macos) and `/apple` on your headscale for more information) | +| iOS | Yes (see [docs](./docs/apple-client.md#ios) and `/apple` on your headscale for more information) | ## Running headscale diff --git a/docs/apple-client.md b/docs/apple-client.md new file mode 100644 index 00000000..29ad4b45 --- /dev/null +++ b/docs/apple-client.md @@ -0,0 +1,51 @@ +# Connecting an Apple client + +## Goal + +This documentation has the goal of showing how a user can use the official iOS and macOS [Tailscale](https://tailscale.com) clients with `headscale`. + +!!! info "Instructions on your headscale instance" + + An endpoint with information on how to connect your Apple device + is also available at `/apple` on your running instance. + +## iOS + +### Installation + +Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037). + +### Configuring the headscale URL + +- Open Tailscale and make sure you are _not_ logged in to any account +- Open Settings on the iOS device +- Scroll down to the `third party apps` section, under `Game Center` or `TV Provider` +- Find Tailscale and select it + - If the iOS device was previously logged into Tailscale, switch the `Reset Keychain` toggle to `on` +- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) under `Alternate Coordination Server URL` +- Restart the app by closing it from the iOS app switcher, open the app and select the regular sign in option + _(non-SSO)_. It should open up to the headscale authentication page. +- Enter your credentials and log in. Headscale should now be working on your iOS device. + +## macOS + +### Installation + +Choose one of the available [Tailscale clients for macOS](https://tailscale.com/kb/1065/macos-variants) and install it. + +### Configuring the headscale URL + +#### Command line + +Use Tailscale's login command to connect with your headscale instance (e.g `https://headscale.example.com`): + +``` +tailscale login --login-server +``` + +#### GUI + +- ALT + Click the Tailscale icon in the menu and hover over the Debug menu +- Under `Custom Login Server`, select `Add Account...` +- Enter the URL of your headscale instance (e.g `https://headscale.example.com`) and press `Add Account` +- Follow the login procedure in the browser diff --git a/docs/iOS-client.md b/docs/iOS-client.md deleted file mode 100644 index 761dfcf0..00000000 --- a/docs/iOS-client.md +++ /dev/null @@ -1,30 +0,0 @@ -# Connecting an iOS client - -## Goal - -This documentation has the goal of showing how a user can use the official iOS [Tailscale](https://tailscale.com) client with `headscale`. - -## Installation - -Install the official Tailscale iOS client from the [App Store](https://apps.apple.com/app/tailscale/id1470499037). - -Ensure that the installed version is at least 1.38.1, as that is the first release to support alternate control servers. - -## Configuring the headscale URL - -!!! info "Apple devices" - - An endpoint with information on how to connect your Apple devices - (currently macOS only) is available at `/apple` on your running instance. - -Ensure that the tailscale app is logged out before proceeding. - -Go to iOS settings, scroll down past game center and tv provider to the tailscale app and select it. The headscale URL can be entered into the _"ALTERNATE COORDINATION SERVER URL"_ box. - -> **Note** -> -> If the app was previously logged into tailscale, toggle on the _Reset Keychain_ switch. - -Restart the app by closing it from the iOS app switcher, open the app and select the regular _Sign in_ option (non-SSO), and it should open up to the headscale authentication page. - -Enter your credentials and log in. Headscale should now be working on your iOS device. diff --git a/docs/windows-client.md b/docs/windows-client.md index ff4834b4..66c47279 100644 --- a/docs/windows-client.md +++ b/docs/windows-client.md @@ -4,17 +4,17 @@ This documentation has the goal of showing how a user can use the official Windows [Tailscale](https://tailscale.com) client with `headscale`. +!!! info "Instructions on your headscale instance" + + An endpoint with information on how to connect your Windows device + is also available at `/windows` on your running instance. + ## Installation Download the [Official Windows Client](https://tailscale.com/download/windows) and install it. ## Configuring the headscale URL -!!! info "Instructions on your headscale instance" - - An endpoint with information on how to connect your Windows device - is also available at `/windows` on your running instance. - Open a Command Prompt or Powershell and use Tailscale's login command to connect with your headscale instance (e.g `https://headscale.example.com`): diff --git a/hscontrol/templates/apple.html b/hscontrol/templates/apple.html index 4064dced..9582594a 100644 --- a/hscontrol/templates/apple.html +++ b/hscontrol/templates/apple.html @@ -25,17 +25,48 @@ +

headscale: iOS configuration

+

GUI

+
    +
  1. + Install the official Tailscale iOS client from the + App store +
  2. +
  3. + Open Tailscale and make sure you are not logged in to any account +
  4. +
  5. Open Settings on the iOS device
  6. +
  7. + Scroll down to the "third party apps" section, under "Game Center" or + "TV Provider" +
  8. +
  9. + Find Tailscale and select it +
      +
    • + If the iOS device was previously logged into Tailscale, switch the + "Reset Keychain" toggle to "on" +
    • +
    +
  10. +
  11. Enter "{{.URL}}" under "Alternate Coordination Server URL"
  12. +
  13. + Restart the app by closing it from the iOS app switcher, open the app + and select the regular sign in option (non-SSO). It should open + up to the headscale authentication page. +
  14. +
  15. + Enter your credentials and log in. Headscale should now be working on + your iOS device +
  16. +

headscale: macOS configuration

-

Recent Tailscale versions (1.34.0 and higher)

-

- Tailscale added Fast User Switching in version 1.34 and you can now use - the new login command to connect to one or more headscale (and Tailscale) - servers. The previously used profiles does not have an effect anymore. -

-

Command line

+

Command line

Use Tailscale's login command to add your profile:

tailscale login --login-server {{.URL}}
-

GUI

+

GUI

  1. ALT + Click the Tailscale icon in the menu and hover over the Debug menu @@ -46,44 +77,7 @@
  2. Follow the login procedure in the browser
-

Apple configuration profiles (1.32.0 and lower)

-

- This page provides - configuration profiles - for the official Tailscale clients for -

- -

- The profiles will configure Tailscale.app to use {{.URL}} as - its control server. -

-

Caution

-

- You should always download and inspect the profile before installing it: -

-
    -
  • - for app store client: curl {{.URL}}/apple/macos-app-store -
  • -
  • - for standalone client: curl {{.URL}}/apple/macos-standalone -
  • -

Profiles

-

macOS

Headscale can be set to the default server by installing a Headscale configuration profile: @@ -121,50 +115,17 @@

Restart Tailscale.app and log in.

-

headscale: iOS configuration

-

Recent Tailscale versions (1.38.1 and higher)

+

Caution

- Tailscale 1.38.1 on - iOS - added a configuration option to allow user to set an "Alternate - Coordination server". This can be used to connect to your headscale - server. + You should always download and inspect the profile before installing it:

-

GUI

-
    +
    • - Install the official Tailscale iOS client from the - App store + for app store client: curl {{.URL}}/apple/macos-app-store
    • - Open Tailscale and make sure you are not logged in to any account + for standalone client: curl {{.URL}}/apple/macos-standalone
    • -
    • Open Settings on the iOS device
    • -
    • - Scroll down to the "third party apps" section, under "Game Center" or - "TV Provider" -
    • -
    • - Find Tailscale and select it -
        -
      • - If the iOS device was previously logged into Tailscale, switch the - "Reset Keychain" toggle to "on" -
      • -
      -
    • -
    • Enter "{{.URL}}" under "Alternate Coordination Server URL"
    • -
    • - Restart the app by closing it from the iOS app switcher, open the app - and select the regular sign in option (non-SSO). It should open - up to the headscale authentication page. -
    • -
    • - Enter your credentials and log in. Headscale should now be working on - your iOS device -
    • -
+ diff --git a/integration/scenario_test.go b/integration/scenario_test.go index ea941ed7..9db4c3a0 100644 --- a/integration/scenario_test.go +++ b/integration/scenario_test.go @@ -7,7 +7,7 @@ import ( ) // This file is intended to "test the test framework", by proxy it will also test -// some Headcsale/Tailscale stuff, but mostly in very simple ways. +// some Headscale/Tailscale stuff, but mostly in very simple ways. func IntegrationSkip(t *testing.T) { t.Helper() diff --git a/mkdocs.yml b/mkdocs.yml index b88cfcc4..fe5c0d64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -139,5 +139,5 @@ nav: - Remote CLI: remote-cli.md - Usage: - Android: android-client.md + - Apple: apple-client.md - Windows: windows-client.md - - iOS: iOS-client.md From fe68f503289db6cb1c2a568b8ae02a45ac632dd6 Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 11 Sep 2024 18:46:06 +0200 Subject: [PATCH 46/60] Use headscale.example.com (#2122) --- docs/exit-node.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exit-node.md b/docs/exit-node.md index 797f42f4..1acd20a3 100644 --- a/docs/exit-node.md +++ b/docs/exit-node.md @@ -5,7 +5,7 @@ Register the node and make it advertise itself as an exit node: ```console -$ sudo tailscale up --login-server https://my-server.com --advertise-exit-node +$ sudo tailscale up --login-server https://headscale.example.com --advertise-exit-node ``` If the node is already registered, it can advertise exit capabilities like this: From e9d9c0773c6acd8db3e4a0759e0d2cd98163e696 Mon Sep 17 00:00:00 2001 From: nblock Date: Mon, 16 Sep 2024 08:13:45 +0200 Subject: [PATCH 47/60] Exclude irrelevant files from mkdocs rendering (#2136) --- mkdocs.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mkdocs.yml b/mkdocs.yml index fe5c0d64..a8e38cdd 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,6 +55,13 @@ theme: favicon: assets/favicon.png logo: ./logo/headscale3-dots.svg +# Excludes +exclude_docs: | + /packaging/README.md + /packaging/postinstall.sh + /packaging/postremove.sh + /requirements.txt + # Plugins plugins: - search: From 6cbbcd859c815031730f035fd8f3ca90d05cf522 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Sep 2024 06:15:45 +0000 Subject: [PATCH 48/60] flake.lock: Update (#2135) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 9b66e4e0..d016082b 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1725534445, - "narHash": "sha256-Yd0FK9SkWy+ZPuNqUgmVPXokxDgMJoGuNpMEtkfcf84=", + "lastModified": 1726238386, + "narHash": "sha256-3//V84fYaGVncFImitM6lSAliRdrGayZLdxWlpcuGk0=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9bb1e7571aadf31ddb4af77fc64b2d59580f9a39", + "rev": "01f064c99c792715054dc7a70e4c1626dbbec0c3", "type": "github" }, "original": { From ed78ecda12d86ffe115e4a10172376f887ac82a1 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 17 Sep 2024 10:44:55 +0100 Subject: [PATCH 49/60] add shutdown that asserts if headscale had panics (#2126) Signed-off-by: Kristoffer Dalby --- integration/acl_test.go | 16 ++++++++-------- integration/auth_oidc_test.go | 4 ++-- integration/auth_web_flow_test.go | 4 ++-- integration/cli_test.go | 30 +++++++++++++++--------------- integration/dns_test.go | 4 ++-- integration/embedded_derp_test.go | 2 +- integration/general_test.go | 20 ++++++++++---------- integration/route_test.go | 10 +++++----- integration/scenario_test.go | 6 +++--- integration/ssh_test.go | 10 +++++----- 10 files changed, 53 insertions(+), 53 deletions(-) diff --git a/integration/acl_test.go b/integration/acl_test.go index f7b59eb7..1da8213d 100644 --- a/integration/acl_test.go +++ b/integration/acl_test.go @@ -276,7 +276,7 @@ func TestACLHostsInNetMapTable(t *testing.T) { hsic.WithACLPolicy(&testCase.policy), ) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) allClients, err := scenario.ListTailscaleClients() assertNoErr(t, err) @@ -316,7 +316,7 @@ func TestACLAllowUser80Dst(t *testing.T) { }, 1, ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) user1Clients, err := scenario.ListTailscaleClients("user1") assertNoErr(t, err) @@ -373,7 +373,7 @@ func TestACLDenyAllPort80(t *testing.T) { }, 4, ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) allClients, err := scenario.ListTailscaleClients() assertNoErr(t, err) @@ -417,7 +417,7 @@ func TestACLAllowUserDst(t *testing.T) { }, 2, ) - // defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) user1Clients, err := scenario.ListTailscaleClients("user1") assertNoErr(t, err) @@ -473,7 +473,7 @@ func TestACLAllowStarDst(t *testing.T) { }, 2, ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) user1Clients, err := scenario.ListTailscaleClients("user1") assertNoErr(t, err) @@ -534,7 +534,7 @@ func TestACLNamedHostsCanReachBySubnet(t *testing.T) { }, 3, ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) user1Clients, err := scenario.ListTailscaleClients("user1") assertNoErr(t, err) @@ -672,7 +672,7 @@ func TestACLNamedHostsCanReach(t *testing.T) { &testCase.policy, 2, ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) // Since user/users dont matter here, we basically expect that some clients // will be assigned these ips and that we can pick them up for our own use. @@ -1021,7 +1021,7 @@ func TestPolicyUpdateWhileRunningWithCLIInDatabase(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 1, diff --git a/integration/auth_oidc_test.go b/integration/auth_oidc_test.go index d24bf452..38435fdc 100644 --- a/integration/auth_oidc_test.go +++ b/integration/auth_oidc_test.go @@ -48,7 +48,7 @@ func TestOIDCAuthenticationPingAll(t *testing.T) { scenario := AuthOIDCScenario{ Scenario: baseScenario, } - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -108,7 +108,7 @@ func TestOIDCExpireNodesBasedOnTokenExpiry(t *testing.T) { scenario := AuthOIDCScenario{ Scenario: baseScenario, } - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 3, diff --git a/integration/auth_web_flow_test.go b/integration/auth_web_flow_test.go index 8e121ca0..2eacd276 100644 --- a/integration/auth_web_flow_test.go +++ b/integration/auth_web_flow_test.go @@ -34,7 +34,7 @@ func TestAuthWebFlowAuthenticationPingAll(t *testing.T) { scenario := AuthWebFlowScenario{ Scenario: baseScenario, } - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -73,7 +73,7 @@ func TestAuthWebFlowLogoutAndRelogin(t *testing.T) { scenario := AuthWebFlowScenario{ Scenario: baseScenario, } - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), diff --git a/integration/cli_test.go b/integration/cli_test.go index fd7a8c1b..aa34dc47 100644 --- a/integration/cli_test.go +++ b/integration/cli_test.go @@ -35,7 +35,7 @@ func TestUserCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 0, @@ -115,7 +115,7 @@ func TestPreAuthKeyCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 0, @@ -257,7 +257,7 @@ func TestPreAuthKeyCommandWithoutExpiry(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 0, @@ -320,7 +320,7 @@ func TestPreAuthKeyCommandReusableEphemeral(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 0, @@ -398,7 +398,7 @@ func TestPreAuthKeyCorrectUserLoggedInCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user1: 1, @@ -492,7 +492,7 @@ func TestApiKeyCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 0, @@ -660,7 +660,7 @@ func TestNodeTagCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 0, @@ -785,7 +785,7 @@ func TestNodeAdvertiseTagNoACLCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 1, @@ -835,7 +835,7 @@ func TestNodeAdvertiseTagWithACLCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": 1, @@ -898,7 +898,7 @@ func TestNodeCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "node-user": 0, @@ -1139,7 +1139,7 @@ func TestNodeExpireCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "node-expire-user": 0, @@ -1266,7 +1266,7 @@ func TestNodeRenameCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "node-rename-command": 0, @@ -1432,7 +1432,7 @@ func TestNodeMoveCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "old-user": 0, @@ -1593,7 +1593,7 @@ func TestPolicyCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "policy-user": 0, @@ -1673,7 +1673,7 @@ func TestPolicyBrokenConfigCommand(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "policy-user": 1, diff --git a/integration/dns_test.go b/integration/dns_test.go index f7973300..085448c5 100644 --- a/integration/dns_test.go +++ b/integration/dns_test.go @@ -17,7 +17,7 @@ func TestResolveMagicDNS(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "magicdns1": len(MustTestVersions), @@ -208,7 +208,7 @@ func TestValidateResolvConf(t *testing.T) { t.Run(tt.name, func(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "resolvconf1": 3, diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 745f2c89..259c565a 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -32,7 +32,7 @@ func TestDERPServerScenario(t *testing.T) { Scenario: baseScenario, tsicNetworks: map[string]*dockertest.Network{}, } - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), diff --git a/integration/general_test.go b/integration/general_test.go index a8421f47..d63b83b3 100644 --- a/integration/general_test.go +++ b/integration/general_test.go @@ -27,7 +27,7 @@ func TestPingAllByIP(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) // TODO(kradalby): it does not look like the user thing works, only second // get created? maybe only when many? @@ -71,7 +71,7 @@ func TestPingAllByIPPublicDERP(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -109,7 +109,7 @@ func TestAuthKeyLogoutAndRelogin(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -228,7 +228,7 @@ func testEphemeralWithOptions(t *testing.T, opts ...hsic.Option) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -313,7 +313,7 @@ func TestEphemeral2006DeletedTooQuickly(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -427,7 +427,7 @@ func TestPingAllByHostname(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user3": len(MustTestVersions), @@ -476,7 +476,7 @@ func TestTaildrop(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "taildrop": len(MustTestVersions), @@ -637,7 +637,7 @@ func TestExpireNode(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -763,7 +763,7 @@ func TestNodeOnlineStatus(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ "user1": len(MustTestVersions), @@ -878,7 +878,7 @@ func TestPingAllByIPManyUpDown(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) // TODO(kradalby): it does not look like the user thing works, only second // get created? maybe only when many? diff --git a/integration/route_test.go b/integration/route_test.go index 0252e702..ca37b99a 100644 --- a/integration/route_test.go +++ b/integration/route_test.go @@ -32,7 +32,7 @@ func TestEnablingRoutes(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 3, @@ -254,7 +254,7 @@ func TestHASubnetRouterFailover(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 3, @@ -826,7 +826,7 @@ func TestEnableDisableAutoApprovedRoute(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 1, @@ -968,7 +968,7 @@ func TestAutoApprovedSubRoute2068(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 1, @@ -1059,7 +1059,7 @@ func TestSubnetRouteACL(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErrf(t, "failed to create scenario: %s", err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) spec := map[string]int{ user: 2, diff --git a/integration/scenario_test.go b/integration/scenario_test.go index 9db4c3a0..aec6cb5c 100644 --- a/integration/scenario_test.go +++ b/integration/scenario_test.go @@ -35,7 +35,7 @@ func TestHeadscale(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) t.Run("start-headscale", func(t *testing.T) { headscale, err := scenario.Headscale() @@ -80,7 +80,7 @@ func TestCreateTailscale(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) scenario.users[user] = &User{ Clients: make(map[string]TailscaleClient), @@ -116,7 +116,7 @@ func TestTailscaleNodesJoiningHeadcale(t *testing.T) { scenario, err := NewScenario(dockertestMaxWait()) assertNoErr(t, err) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) t.Run("start-headscale", func(t *testing.T) { headscale, err := scenario.Headscale() diff --git a/integration/ssh_test.go b/integration/ssh_test.go index 6d053b0d..c31cc108 100644 --- a/integration/ssh_test.go +++ b/integration/ssh_test.go @@ -111,7 +111,7 @@ func TestSSHOneUserToAll(t *testing.T) { }, len(MustTestVersions), ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) allClients, err := scenario.ListTailscaleClients() assertNoErrListClients(t, err) @@ -176,7 +176,7 @@ func TestSSHMultipleUsersAllToAll(t *testing.T) { }, len(MustTestVersions), ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) nsOneClients, err := scenario.ListTailscaleClients("user1") assertNoErrListClients(t, err) @@ -222,7 +222,7 @@ func TestSSHNoSSHConfigured(t *testing.T) { }, len(MustTestVersions), ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) allClients, err := scenario.ListTailscaleClients() assertNoErrListClients(t, err) @@ -271,7 +271,7 @@ func TestSSHIsBlockedInACL(t *testing.T) { }, len(MustTestVersions), ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) allClients, err := scenario.ListTailscaleClients() assertNoErrListClients(t, err) @@ -327,7 +327,7 @@ func TestSSHUserOnlyIsolation(t *testing.T) { }, len(MustTestVersions), ) - defer scenario.Shutdown() + defer scenario.ShutdownAssertNoPanics(t) ssh1Clients, err := scenario.ListTailscaleClients("user1") assertNoErrListClients(t, err) From 10a72e8d542af68c0c280f2a6ccc84849719b24c Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 18 Sep 2024 09:43:08 +0100 Subject: [PATCH 50/60] update changelog for 0.23 release (#2138) Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9818217..f8035d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,9 @@ # CHANGELOG -## 0.23.0 (2023-XX-XX) +## 0.23.0 (2023-09-18) -This release is mainly a code reorganisation and refactoring, significantly improving the maintainability of the codebase. This should allow us to improve further and make it easier for the maintainers to keep on top of the project. +This release was intended to be mainly a code reorganisation and refactoring, significantly improving the maintainability of the codebase. This should allow us to improve further and make it easier for the maintainers to keep on top of the project. +However, as you all have noticed, it turned out to become a much larger, much longer release cycle than anticipated. It has ended up to be a release with a lot of rewrites and changes to the code base and functionality of Headscale, cleaning up a lot of technical debt and introducing a lot of improvements. This does come with some breaking changes, **Please remember to always back up your database between versions** @@ -16,7 +17,7 @@ The [“poller”, or streaming logic](https://github.com/juanfont/headscale/blo Headscale now supports sending “delta” updates, thanks to the new mapper and poller logic, allowing us to only inform nodes about new nodes, changed nodes and removed nodes. Previously we sent the entire state of the network every time an update was due. -While we have a pretty good [test harness](https://github.com/search?q=repo%3Ajuanfont%2Fheadscale+path%3A_test.go&type=code) for validating our changes, we have rewritten over [10000 lines of code](https://github.com/juanfont/headscale/compare/b01f1f1867136d9b2d7b1392776eb363b482c525...main) and bugs are expected. We need help testing this release. In addition, while we think the performance should in general be better, there might be regressions in parts of the platform, particularly where we prioritised correctness over speed. +While we have a pretty good [test harness](https://github.com/search?q=repo%3Ajuanfont%2Fheadscale+path%3A_test.go&type=code) for validating our changes, the changes came down to [284 changed files with 32,316 additions and 24,245 deletions](https://github.com/juanfont/headscale/compare/b01f1f1867136d9b2d7b1392776eb363b482c525...ed78ecd) and bugs are expected. We need help testing this release. In addition, while we think the performance should in general be better, there might be regressions in parts of the platform, particularly where we prioritised correctness over speed. There are also several bugfixes that has been encountered and fixed as part of implementing these changes, particularly after improving the test harness as part of adopting [#1460](https://github.com/juanfont/headscale/pull/1460). From 1e610848980012e5fe49c1b5d68bdef08854d419 Mon Sep 17 00:00:00 2001 From: enoperm <61619504+enoperm@users.noreply.github.com> Date: Sat, 21 Sep 2024 12:05:36 +0200 Subject: [PATCH 51/60] Add compatibility with only websocket-capable clients (#2132) * handle control protocol through websocket The necessary behaviour is already in place, but the wasm build only issued GETs, and the handler was not invoked. * get DERP-over-websocket working for wasm clients * Prepare for testing builtin websocket-over-DERP Still needs some way to assert that clients are connected through websockets, rather than the TCP hijacking version of DERP. * integration tests: properly differentiate between DERP transports * do not touch unrelated code * linter fixes * integration testing: unexport common implementation of derp server scenario * fixup! integration testing: unexport common implementation of derp server scenario * dockertestutil/logs: remove unhelpful comment * update changelog --------- Co-authored-by: Csaba Sarkadi --- .github/workflows/test-integration.yaml | 1 + CHANGELOG.md | 6 +- go.mod | 2 +- hscontrol/app.go | 2 +- hscontrol/derp/server/derp_server.go | 53 +++++++++++ hscontrol/types/users.go | 2 +- hscontrol/util/net.go | 1 - integration/dns_test.go | 1 - integration/dockertestutil/logs.go | 42 +++++---- integration/embedded_derp_test.go | 118 +++++++++++++++++++++--- integration/hsic/hsic.go | 6 ++ integration/tailscale.go | 3 + integration/tsic/tsic.go | 37 ++++++++ integration/utils.go | 43 ++++++++- 14 files changed, 280 insertions(+), 37 deletions(-) diff --git a/.github/workflows/test-integration.yaml b/.github/workflows/test-integration.yaml index d6c7eff2..80daf20a 100644 --- a/.github/workflows/test-integration.yaml +++ b/.github/workflows/test-integration.yaml @@ -41,6 +41,7 @@ jobs: - TestResolveMagicDNS - TestValidateResolvConf - TestDERPServerScenario + - TestDERPServerWebsocketScenario - TestPingAllByIP - TestPingAllByIPPublicDERP - TestAuthKeyLogoutAndRelogin diff --git a/CHANGELOG.md b/CHANGELOG.md index f8035d51..7e91082c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # CHANGELOG -## 0.23.0 (2023-09-18) +## Next + +- Improved compatibilty of built-in DERP server with clients connecting over WebSocket. + +## 0.23.0 (2024-09-18) This release was intended to be mainly a code reorganisation and refactoring, significantly improving the maintainability of the codebase. This should allow us to improve further and make it easier for the maintainers to keep on top of the project. However, as you all have noticed, it turned out to become a much larger, much longer release cycle than anticipated. It has ended up to be a release with a lot of rewrites and changes to the code base and functionality of Headscale, cleaning up a lot of technical debt and introducing a lot of improvements. This does come with some breaking changes, diff --git a/go.mod b/go.mod index 18089bbd..73893d82 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.23.0 require ( github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/coder/websocket v1.8.12 github.com/coreos/go-oidc/v3 v3.11.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/deckarep/golang-set/v2 v2.6.0 @@ -79,7 +80,6 @@ require ( github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/coder/websocket v1.8.12 // indirect github.com/containerd/console v1.0.4 // indirect github.com/containerd/continuity v0.4.3 // indirect github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect diff --git a/hscontrol/app.go b/hscontrol/app.go index 4a5b4679..1d3cb629 100644 --- a/hscontrol/app.go +++ b/hscontrol/app.go @@ -425,7 +425,7 @@ func (h *Headscale) createRouter(grpcMux *grpcRuntime.ServeMux) *mux.Router { router := mux.NewRouter() router.Use(prometheusMiddleware) - router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost) + router.HandleFunc(ts2021UpgradePath, h.NoiseUpgradeHandler).Methods(http.MethodPost, http.MethodGet) router.HandleFunc("/health", h.HealthHandler).Methods(http.MethodGet) router.HandleFunc("/key", h.KeyHandler).Methods(http.MethodGet) diff --git a/hscontrol/derp/server/derp_server.go b/hscontrol/derp/server/derp_server.go index 0b0c9b16..0c97806f 100644 --- a/hscontrol/derp/server/derp_server.go +++ b/hscontrol/derp/server/derp_server.go @@ -1,6 +1,7 @@ package server import ( + "bufio" "context" "encoding/json" "fmt" @@ -12,11 +13,13 @@ import ( "strings" "time" + "github.com/coder/websocket" "github.com/juanfont/headscale/hscontrol/types" "github.com/juanfont/headscale/hscontrol/util" "github.com/rs/zerolog/log" "tailscale.com/derp" "tailscale.com/net/stun" + "tailscale.com/net/wsconn" "tailscale.com/tailcfg" "tailscale.com/types/key" ) @@ -132,6 +135,56 @@ func (d *DERPServer) DERPHandler( return } + if strings.Contains(req.Header.Get("Sec-Websocket-Protocol"), "derp") { + d.serveWebsocket(writer, req) + } else { + d.servePlain(writer, req) + } +} + +func (d *DERPServer) serveWebsocket(writer http.ResponseWriter, req *http.Request) { + websocketConn, err := websocket.Accept(writer, req, &websocket.AcceptOptions{ + Subprotocols: []string{"derp"}, + OriginPatterns: []string{"*"}, + // Disable compression because DERP transmits WireGuard messages that + // are not compressible. + // Additionally, Safari has a broken implementation of compression + // (see https://github.com/nhooyr/websocket/issues/218) that makes + // enabling it actively harmful. + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to upgrade websocket request") + + writer.Header().Set("Content-Type", "text/plain") + writer.WriteHeader(http.StatusInternalServerError) + + _, err = writer.Write([]byte("Failed to upgrade websocket request")) + if err != nil { + log.Error(). + Caller(). + Err(err). + Msg("Failed to write response") + } + + return + } + defer websocketConn.Close(websocket.StatusInternalError, "closing") + if websocketConn.Subprotocol() != "derp" { + websocketConn.Close(websocket.StatusPolicyViolation, "client must speak the derp subprotocol") + + return + } + + wc := wsconn.NetConn(req.Context(), websocketConn, websocket.MessageBinary, req.RemoteAddr) + brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc)) + d.tailscaleDERP.Accept(req.Context(), wc, brw, req.RemoteAddr) +} + +func (d *DERPServer) servePlain(writer http.ResponseWriter, req *http.Request) { fastStart := req.Header.Get(fastStartHeader) == "1" hijacker, ok := writer.(http.Hijacker) diff --git a/hscontrol/types/users.go b/hscontrol/types/users.go index 63e73a56..3e934e34 100644 --- a/hscontrol/types/users.go +++ b/hscontrol/types/users.go @@ -19,7 +19,7 @@ type User struct { Name string `gorm:"unique"` } -// TODO(kradalby): See if we can fill in Gravatar here +// TODO(kradalby): See if we can fill in Gravatar here. func (u *User) profilePicURL() string { return "" } diff --git a/hscontrol/util/net.go b/hscontrol/util/net.go index c44b7287..59a8d635 100644 --- a/hscontrol/util/net.go +++ b/hscontrol/util/net.go @@ -13,7 +13,6 @@ func GrpcSocketDialer(ctx context.Context, addr string) (net.Conn, error) { return d.DialContext(ctx, "unix", addr) } - // TODO(kradalby): Remove after go 1.24, will be in stdlib. // Compare returns an integer comparing two prefixes. // The result will be 0 if p == p2, -1 if p < p2, and +1 if p > p2. diff --git a/integration/dns_test.go b/integration/dns_test.go index 085448c5..efe702e9 100644 --- a/integration/dns_test.go +++ b/integration/dns_test.go @@ -242,5 +242,4 @@ func TestValidateResolvConf(t *testing.T) { } }) } - } diff --git a/integration/dockertestutil/logs.go b/integration/dockertestutil/logs.go index 64c3c9ac..7d104e43 100644 --- a/integration/dockertestutil/logs.go +++ b/integration/dockertestutil/logs.go @@ -3,6 +3,7 @@ package dockertestutil import ( "bytes" "context" + "io" "log" "os" "path" @@ -13,6 +14,28 @@ import ( const filePerm = 0o644 +func WriteLog( + pool *dockertest.Pool, + resource *dockertest.Resource, + stdout io.Writer, + stderr io.Writer, +) error { + return pool.Client.Logs( + docker.LogsOptions{ + Context: context.TODO(), + Container: resource.Container.ID, + OutputStream: stdout, + ErrorStream: stderr, + Tail: "all", + RawTerminal: false, + Stdout: true, + Stderr: true, + Follow: false, + Timestamps: false, + }, + ) +} + func SaveLog( pool *dockertest.Pool, resource *dockertest.Resource, @@ -23,23 +46,8 @@ func SaveLog( return "", "", err } - var stdout bytes.Buffer - var stderr bytes.Buffer - - err = pool.Client.Logs( - docker.LogsOptions{ - Context: context.TODO(), - Container: resource.Container.ID, - OutputStream: &stdout, - ErrorStream: &stderr, - Tail: "all", - RawTerminal: false, - Stdout: true, - Stderr: true, - Follow: false, - Timestamps: false, - }, - ) + var stdout, stderr bytes.Buffer + err = WriteLog(pool, resource, &stdout, &stderr) if err != nil { return "", "", err } diff --git a/integration/embedded_derp_test.go b/integration/embedded_derp_test.go index 259c565a..6009aed5 100644 --- a/integration/embedded_derp_test.go +++ b/integration/embedded_derp_test.go @@ -15,6 +15,11 @@ import ( "github.com/ory/dockertest/v3" ) +type ClientsSpec struct { + Plain int + WebsocketDERP int +} + type EmbeddedDERPServerScenario struct { *Scenario @@ -22,6 +27,65 @@ type EmbeddedDERPServerScenario struct { } func TestDERPServerScenario(t *testing.T) { + spec := map[string]ClientsSpec{ + "user1": { + Plain: len(MustTestVersions), + WebsocketDERP: 0, + }, + } + + derpServerScenario(t, spec, func(scenario *EmbeddedDERPServerScenario) { + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + t.Logf("checking %d clients for websocket connections", len(allClients)) + + for _, client := range allClients { + if didClientUseWebsocketForDERP(t, client) { + t.Logf( + "client %q used websocket a connection, but was not expected to", + client.Hostname(), + ) + t.Fail() + } + } + }) +} + +func TestDERPServerWebsocketScenario(t *testing.T) { + spec := map[string]ClientsSpec{ + "user1": { + Plain: 0, + WebsocketDERP: len(MustTestVersions), + }, + } + + derpServerScenario(t, spec, func(scenario *EmbeddedDERPServerScenario) { + allClients, err := scenario.ListTailscaleClients() + assertNoErrListClients(t, err) + t.Logf("checking %d clients for websocket connections", len(allClients)) + + for _, client := range allClients { + if !didClientUseWebsocketForDERP(t, client) { + t.Logf( + "client %q does not seem to have used a websocket connection, even though it was expected to do so", + client.Hostname(), + ) + t.Fail() + } + } + }) +} + +// This function implements the common parts of a DERP scenario, +// we *want* it to show up in stacktraces, +// so marking it as a test helper would be counterproductive. +// +//nolint:thelper +func derpServerScenario( + t *testing.T, + spec map[string]ClientsSpec, + furtherAssertions ...func(*EmbeddedDERPServerScenario), +) { IntegrationSkip(t) // t.Parallel() @@ -34,20 +98,18 @@ func TestDERPServerScenario(t *testing.T) { } defer scenario.ShutdownAssertNoPanics(t) - spec := map[string]int{ - "user1": len(MustTestVersions), - } - err = scenario.CreateHeadscaleEnv( spec, hsic.WithTestName("derpserver"), hsic.WithExtraPorts([]string{"3478/udp"}), hsic.WithEmbeddedDERPServerOnly(), + hsic.WithPort(443), hsic.WithTLS(), hsic.WithHostnameAsServerURL(), hsic.WithConfigEnv(map[string]string{ "HEADSCALE_DERP_AUTO_UPDATE_ENABLED": "true", "HEADSCALE_DERP_UPDATE_FREQUENCY": "10s", + "HEADSCALE_LISTEN_ADDR": "0.0.0.0:443", }), ) assertNoErrHeadscaleEnv(t, err) @@ -76,6 +138,11 @@ func TestDERPServerScenario(t *testing.T) { } success := pingDerpAllHelper(t, allClients, allHostnames) + if len(allHostnames)*len(allClients) > success { + t.FailNow() + + return + } for _, client := range allClients { status, err := client.Status() @@ -98,6 +165,9 @@ func TestDERPServerScenario(t *testing.T) { time.Sleep(30 * time.Second) success = pingDerpAllHelper(t, allClients, allHostnames) + if len(allHostnames)*len(allClients) > success { + t.Fail() + } for _, client := range allClients { status, err := client.Status() @@ -114,10 +184,14 @@ func TestDERPServerScenario(t *testing.T) { } t.Logf("Run2: %d successful pings out of %d", success, len(allClients)*len(allHostnames)) + + for _, check := range furtherAssertions { + check(&scenario) + } } func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( - users map[string]int, + users map[string]ClientsSpec, opts ...hsic.Option, ) error { hsServer, err := s.Headscale(opts...) @@ -137,6 +211,7 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( if err != nil { return err } + log.Printf("headscale server ip address: %s", hsServer.GetIP()) hash, err := util.GenerateRandomStringDNSSafe(scenarioHashLength) if err != nil { @@ -149,14 +224,31 @@ func (s *EmbeddedDERPServerScenario) CreateHeadscaleEnv( return err } - err = s.CreateTailscaleIsolatedNodesInUser( - hash, - userName, - "all", - clientCount, - ) - if err != nil { - return err + if clientCount.Plain > 0 { + // Containers that use default DERP config + err = s.CreateTailscaleIsolatedNodesInUser( + hash, + userName, + "all", + clientCount.Plain, + ) + if err != nil { + return err + } + } + + if clientCount.WebsocketDERP > 0 { + // Containers that use DERP-over-WebSocket + err = s.CreateTailscaleIsolatedNodesInUser( + hash, + userName, + "all", + clientCount.WebsocketDERP, + tsic.WithWebsocketDERP(true), + ) + if err != nil { + return err + } } key, err := s.CreatePreAuthKey(userName, true, false) diff --git a/integration/hsic/hsic.go b/integration/hsic/hsic.go index 20a778b8..c2ae3336 100644 --- a/integration/hsic/hsic.go +++ b/integration/hsic/hsic.go @@ -461,6 +461,12 @@ func (t *HeadscaleInContainer) Shutdown() (string, string, error) { return stdoutPath, stderrPath, t.pool.Purge(t.container) } +// WriteLogs writes the current stdout/stderr log of the container to +// the given io.Writers. +func (t *HeadscaleInContainer) WriteLogs(stdout, stderr io.Writer) error { + return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr) +} + // SaveLog saves the current stdout log of the container to a path // on the host system. func (t *HeadscaleInContainer) SaveLog(path string) (string, string, error) { diff --git a/integration/tailscale.go b/integration/tailscale.go index 5b1baf1b..f858d2c2 100644 --- a/integration/tailscale.go +++ b/integration/tailscale.go @@ -1,6 +1,7 @@ package integration import ( + "io" "net/netip" "net/url" @@ -41,4 +42,6 @@ type TailscaleClient interface { // 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. FailingPeersAsString() (string, bool, error) + + WriteLogs(stdout, stderr io.Writer) error } diff --git a/integration/tsic/tsic.go b/integration/tsic/tsic.go index a3fac17c..944bb94d 100644 --- a/integration/tsic/tsic.go +++ b/integration/tsic/tsic.go @@ -67,6 +67,7 @@ type TailscaleInContainer struct { // optional config headscaleCert []byte headscaleHostname string + withWebsocketDERP bool withSSH bool withTags []string withEntrypoint []string @@ -126,6 +127,14 @@ func WithTags(tags []string) Option { } } +// WithWebsocketDERP toggles a development knob to +// force enable DERP connection through the new websocket protocol. +func WithWebsocketDERP(enabled bool) Option { + return func(tsic *TailscaleInContainer) { + tsic.withWebsocketDERP = enabled + } +} + // WithSSH enables SSH for the Tailscale instance. func WithSSH() Option { return func(tsic *TailscaleInContainer) { @@ -206,6 +215,14 @@ func New( // }, Entrypoint: tsic.withEntrypoint, ExtraHosts: tsic.withExtraHosts, + Env: []string{}, + } + + if tsic.withWebsocketDERP { + tailscaleOptions.Env = append( + tailscaleOptions.Env, + fmt.Sprintf("TS_DEBUG_DERP_WS_CLIENT=%t", tsic.withWebsocketDERP), + ) } if tsic.headscaleHostname != "" { @@ -351,6 +368,15 @@ func (t *TailscaleInContainer) Execute( return stdout, stderr, nil } +// Retrieve container logs. +func (t *TailscaleInContainer) Logs(stdout, stderr io.Writer) error { + return dockertestutil.WriteLog( + t.pool, + t.container, + stdout, stderr, + ) +} + // Up runs the login routine on the given Tailscale instance. // This login mechanism uses the authorised key for authentication. func (t *TailscaleInContainer) Login( @@ -999,10 +1025,21 @@ func (t *TailscaleInContainer) WriteFile(path string, data []byte) error { // on the host system. func (t *TailscaleInContainer) SaveLog(path string) error { // TODO(kradalby): Assert if tailscale logs contains panics. + // NOTE(enoperm): `t.WriteLog | countMatchingLines` + // is probably most of what is for that, + // but I'd rather not change the behaviour here, + // as it may affect all the other tests + // I have not otherwise touched. _, _, err := dockertestutil.SaveLog(t.pool, t.container, path) return err } +// WriteLogs writes the current stdout/stderr log of the container to +// the given io.Writers. +func (t *TailscaleInContainer) WriteLogs(stdout, stderr io.Writer) error { + return dockertestutil.WriteLog(t.pool, t.container, stdout, stderr) +} + // 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) { diff --git a/integration/utils.go b/integration/utils.go index 840dbc4c..ec6aeecf 100644 --- a/integration/utils.go +++ b/integration/utils.go @@ -1,6 +1,9 @@ package integration import ( + "bufio" + "bytes" + "io" "os" "strings" "sync" @@ -78,6 +81,25 @@ func assertContains(t *testing.T, str, subStr string) { } } +func didClientUseWebsocketForDERP(t *testing.T, client TailscaleClient) bool { + t.Helper() + + buf := &bytes.Buffer{} + err := client.WriteLogs(buf, buf) + if err != nil { + t.Fatalf("failed to fetch client logs: %s: %s", client.Hostname(), err) + } + + count, err := countMatchingLines(buf, func(line string) bool { + return strings.Contains(line, "websocket: connected to ") + }) + if err != nil { + t.Fatalf("failed to process client logs: %s: %s", client.Hostname(), err) + } + + return count > 0 +} + func pingAllHelper(t *testing.T, clients []TailscaleClient, addrs []string, opts ...tsic.PingOption) int { t.Helper() success := 0 @@ -113,7 +135,7 @@ func pingDerpAllHelper(t *testing.T, clients []TailscaleClient, addrs []string) tsic.WithPingUntilDirect(false), ) if err != nil { - t.Fatalf("failed to ping %s from %s: %s", addr, client.Hostname(), err) + t.Logf("failed to ping %s from %s: %s", addr, client.Hostname(), err) } else { success++ } @@ -321,6 +343,25 @@ func dockertestMaxWait() time.Duration { return wait } +func countMatchingLines(in io.Reader, predicate func(string) bool) (int, error) { + count := 0 + scanner := bufio.NewScanner(in) + { + const logBufferInitialSize = 1024 << 10 // preallocate 1 MiB + buff := make([]byte, logBufferInitialSize) + scanner.Buffer(buff, len(buff)) + scanner.Split(bufio.ScanLines) + } + + for scanner.Scan() { + if predicate(scanner.Text()) { + count += 1 + } + } + + return count, scanner.Err() +} + // func dockertestCommandTimeout() time.Duration { // timeout := 10 * time.Second //nolint // From f3fca8302a85394f9a0adef903794fea53a39818 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 09:46:04 +0000 Subject: [PATCH 52/60] flake.lock: Update (#2143) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index d016082b..935f2263 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1726238386, - "narHash": "sha256-3//V84fYaGVncFImitM6lSAliRdrGayZLdxWlpcuGk0=", + "lastModified": 1726871744, + "narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01f064c99c792715054dc7a70e4c1626dbbec0c3", + "rev": "a1d92660c6b3b7c26fb883500a80ea9d33321be2", "type": "github" }, "original": { From 07b596d3cc9765e412a7faaaef6a663782a6f4dd Mon Sep 17 00:00:00 2001 From: David Mell Date: Mon, 23 Sep 2024 01:59:16 -0800 Subject: [PATCH 53/60] Allow nodes to use SSH agent forwarding (#2145) --- CHANGELOG.md | 1 + hscontrol/policy/acls.go | 4 ++-- hscontrol/policy/acls_test.go | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e91082c..538d1432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Next - Improved compatibilty of built-in DERP server with clients connecting over WebSocket. +- Allow nodes to use SSH agent forwarding [#2145](https://github.com/juanfont/headscale/pull/2145) ## 0.23.0 (2024-09-18) diff --git a/hscontrol/policy/acls.go b/hscontrol/policy/acls.go index 2b3a50f7..b166df03 100644 --- a/hscontrol/policy/acls.go +++ b/hscontrol/policy/acls.go @@ -292,7 +292,7 @@ func (pol *ACLPolicy) CompileSSHPolicy( Reject: false, Accept: true, SessionDuration: 0, - AllowAgentForwarding: false, + AllowAgentForwarding: true, HoldAndDelegate: "", AllowLocalPortForwarding: true, } @@ -401,7 +401,7 @@ func sshCheckAction(duration string) (*tailcfg.SSHAction, error) { Reject: false, Accept: true, SessionDuration: sessionLength, - AllowAgentForwarding: false, + AllowAgentForwarding: true, HoldAndDelegate: "", AllowLocalPortForwarding: true, }, nil diff --git a/hscontrol/policy/acls_test.go b/hscontrol/policy/acls_test.go index 6b2e0f97..9f38c6db 100644 --- a/hscontrol/policy/acls_test.go +++ b/hscontrol/policy/acls_test.go @@ -3323,7 +3323,7 @@ func TestSSHRules(t *testing.T) { SSHUsers: map[string]string{ "autogroup:nonroot": "=", }, - Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true}, + Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true}, }, { SSHUsers: map[string]string{ @@ -3334,7 +3334,7 @@ func TestSSHRules(t *testing.T) { Any: true, }, }, - Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true}, + Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true}, }, { Principals: []*tailcfg.SSHPrincipal{ @@ -3345,7 +3345,7 @@ func TestSSHRules(t *testing.T) { SSHUsers: map[string]string{ "autogroup:nonroot": "=", }, - Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true}, + Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true}, }, { SSHUsers: map[string]string{ @@ -3356,7 +3356,7 @@ func TestSSHRules(t *testing.T) { Any: true, }, }, - Action: &tailcfg.SSHAction{Accept: true, AllowLocalPortForwarding: true}, + Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true, AllowLocalPortForwarding: true}, }, }}, }, From 4f2fb65929ecc302be745d6398b808baa620aef6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Tue, 24 Sep 2024 18:34:20 +0200 Subject: [PATCH 54/60] remove versions older than 1.56 (#2149) * remove versions older than 1.56 Signed-off-by: Kristoffer Dalby * remove code no longer needed for new clients Signed-off-by: Kristoffer Dalby * update changelog Signed-off-by: Kristoffer Dalby --------- Signed-off-by: Kristoffer Dalby --- CHANGELOG.md | 2 + hscontrol/mapper/mapper_test.go | 20 ++++---- hscontrol/mapper/tail.go | 30 +++--------- hscontrol/mapper/tail_test.go | 17 +++---- hscontrol/metrics.go | 5 -- hscontrol/noise.go | 19 ++++---- hscontrol/poll.go | 81 --------------------------------- integration/scenario.go | 30 ++++++------ 8 files changed, 52 insertions(+), 152 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 538d1432..2bf0a6d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next +- Remove versions older than 1.56 [#2149](https://github.com/juanfont/headscale/pull/2149) + - Clean up old code required by old versions - Improved compatibilty of built-in DERP server with clients connecting over WebSocket. - Allow nodes to use SSH agent forwarding [#2145](https://github.com/juanfont/headscale/pull/2145) diff --git a/hscontrol/mapper/mapper_test.go b/hscontrol/mapper/mapper_test.go index 0484fc02..01f27261 100644 --- a/hscontrol/mapper/mapper_test.go +++ b/hscontrol/mapper/mapper_test.go @@ -244,11 +244,11 @@ func Test_fullMapResponse(t *testing.T) { PrimaryRoutes: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/24")}, LastSeen: &lastSeen, MachineAuthorized: true, - Capabilities: []tailcfg.NodeCapability{ - tailcfg.CapabilityFileSharing, - tailcfg.CapabilityAdmin, - tailcfg.CapabilitySSH, - tailcfg.NodeAttrDisableUPnP, + + CapMap: tailcfg.NodeCapMap{ + tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, + tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, + tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, }, } @@ -299,11 +299,11 @@ func Test_fullMapResponse(t *testing.T) { PrimaryRoutes: []netip.Prefix{}, LastSeen: &lastSeen, MachineAuthorized: true, - Capabilities: []tailcfg.NodeCapability{ - tailcfg.CapabilityFileSharing, - tailcfg.CapabilityAdmin, - tailcfg.CapabilitySSH, - tailcfg.NodeAttrDisableUPnP, + + CapMap: tailcfg.NodeCapMap{ + tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, + tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, + tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, }, } diff --git a/hscontrol/mapper/tail.go b/hscontrol/mapper/tail.go index b0878d1a..a8ccf978 100644 --- a/hscontrol/mapper/tail.go +++ b/hscontrol/mapper/tail.go @@ -114,32 +114,14 @@ func tailNode( Expired: node.IsExpired(), } - // - 74: 2023-09-18: Client understands NodeCapMap - if capVer >= 74 { - tNode.CapMap = tailcfg.NodeCapMap{ - tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, - tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, - tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, - } - - if cfg.RandomizeClientPort { - tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{} - } - } else { - tNode.Capabilities = []tailcfg.NodeCapability{ - tailcfg.CapabilityFileSharing, - tailcfg.CapabilityAdmin, - tailcfg.CapabilitySSH, - } - - if cfg.RandomizeClientPort { - tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrRandomizeClientPort) - } + tNode.CapMap = tailcfg.NodeCapMap{ + tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, + tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, + tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, } - // - 72: 2023-08-23: TS-2023-006 UPnP issue fixed; UPnP can now be used again - if capVer < 72 { - tNode.Capabilities = append(tNode.Capabilities, tailcfg.NodeAttrDisableUPnP) + if cfg.RandomizeClientPort { + tNode.CapMap[tailcfg.NodeAttrRandomizeClientPort] = []tailcfg.RawMessage{} } if node.IsOnline == nil || !*node.IsOnline { diff --git a/hscontrol/mapper/tail_test.go b/hscontrol/mapper/tail_test.go index f744c9c6..c0d1c146 100644 --- a/hscontrol/mapper/tail_test.go +++ b/hscontrol/mapper/tail_test.go @@ -72,9 +72,11 @@ func TestTailNode(t *testing.T) { Tags: []string{}, PrimaryRoutes: []netip.Prefix{}, MachineAuthorized: true, - Capabilities: []tailcfg.NodeCapability{ - "https://tailscale.com/cap/file-sharing", "https://tailscale.com/cap/is-admin", - "https://tailscale.com/cap/ssh", "debug-disable-upnp", + + CapMap: tailcfg.NodeCapMap{ + tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, + tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, + tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, }, }, wantErr: false, @@ -166,11 +168,10 @@ func TestTailNode(t *testing.T) { LastSeen: &lastSeen, MachineAuthorized: true, - Capabilities: []tailcfg.NodeCapability{ - tailcfg.CapabilityFileSharing, - tailcfg.CapabilityAdmin, - tailcfg.CapabilitySSH, - tailcfg.NodeAttrDisableUPnP, + CapMap: tailcfg.NodeCapMap{ + tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, + tailcfg.CapabilityAdmin: []tailcfg.RawMessage{}, + tailcfg.CapabilitySSH: []tailcfg.RawMessage{}, }, }, wantErr: false, diff --git a/hscontrol/metrics.go b/hscontrol/metrics.go index 4870e74e..0be59eec 100644 --- a/hscontrol/metrics.go +++ b/hscontrol/metrics.go @@ -37,11 +37,6 @@ var ( Name: "mapresponse_updates_received_total", Help: "total count of mapresponse updates received on update channel", }, []string{"type"}) - mapResponseWriteUpdatesInStream = promauto.NewCounterVec(prometheus.CounterOpts{ - Namespace: prometheusNamespace, - Name: "mapresponse_write_updates_in_stream_total", - Help: "total count of writes that occurred in a stream session, pre-68 nodes", - }, []string{"status"}) mapResponseEndpointUpdates = promauto.NewCounterVec(prometheus.CounterOpts{ Namespace: prometheusNamespace, Name: "mapresponse_endpoint_updates_total", diff --git a/hscontrol/noise.go b/hscontrol/noise.go index 554be65c..35450809 100644 --- a/hscontrol/noise.go +++ b/hscontrol/noise.go @@ -166,7 +166,7 @@ func (ns *noiseServer) earlyNoise(protocolVersion int, writer io.Writer) error { } const ( - MinimumCapVersion tailcfg.CapabilityVersion = 61 + MinimumCapVersion tailcfg.CapabilityVersion = 82 ) // NoisePollNetMapHandler takes care of /machine/:id/map using the Noise protocol @@ -182,15 +182,6 @@ func (ns *noiseServer) NoisePollNetMapHandler( writer http.ResponseWriter, req *http.Request, ) { - log.Trace(). - Str("handler", "NoisePollNetMap"). - Msg("PollNetMapHandler called") - - log.Trace(). - Any("headers", req.Header). - Caller(). - Msg("Headers") - body, _ := io.ReadAll(req.Body) mapRequest := tailcfg.MapRequest{} @@ -204,6 +195,14 @@ func (ns *noiseServer) NoisePollNetMapHandler( return } + log.Trace(). + Caller(). + Str("handler", "NoisePollNetMap"). + Any("headers", req.Header). + Str("node", mapRequest.Hostinfo.Hostname). + Int("capver", int(mapRequest.Version)). + Msg("PollNetMapHandler called") + // Reject unsupported versions if mapRequest.Version < MinimumCapVersion { log.Info(). diff --git a/hscontrol/poll.go b/hscontrol/poll.go index 82a5295f..252f338b 100644 --- a/hscontrol/poll.go +++ b/hscontrol/poll.go @@ -214,21 +214,6 @@ func (m *mapSession) serveLongPoll() { m.infof("node has disconnected, mapSession: %p, chan: %p", m, m.ch) }() - // From version 68, all streaming requests can be treated as read only. - // TODO: Remove when we drop support for 1.48 - if m.capVer < 68 { - // Error has been handled/written to client in the func - // return - err := m.handleSaveNode() - if err != nil { - mapResponseWriteUpdatesInStream.WithLabelValues("error").Inc() - - m.close() - return - } - mapResponseWriteUpdatesInStream.WithLabelValues("ok").Inc() - } - // Set up the client stream m.h.pollNetMapStreamWG.Add(1) defer m.h.pollNetMapStreamWG.Done() @@ -549,72 +534,6 @@ func (m *mapSession) handleEndpointUpdate() { return } -// handleSaveNode saves node updates in the maprequest _streaming_ -// path and is mostly the same code as in handleEndpointUpdate. -// It is not attempted to be deduplicated since it will go away -// when we stop supporting older than 68 which removes updates -// when the node is streaming. -func (m *mapSession) handleSaveNode() error { - m.tracef("saving node update from stream session") - - change := m.node.PeerChangeFromMapRequest(m.req) - - // A stream is being set up, the node is Online - online := true - change.Online = &online - - m.node.ApplyPeerChange(&change) - - sendUpdate, routesChanged := hostInfoChanged(m.node.Hostinfo, m.req.Hostinfo) - m.node.Hostinfo = m.req.Hostinfo - - // If there is no changes and nothing to save, - // return early. - if peerChangeEmpty(change) || !sendUpdate { - return nil - } - - // Check if the Hostinfo of the node has changed. - // If it has changed, check if there has been a change to - // the routable IPs of the host and update update them in - // the database. Then send a Changed update - // (containing the whole node object) to peers to inform about - // the route change. - // If the hostinfo has changed, but not the routes, just update - // hostinfo and let the function continue. - if routesChanged { - var err error - _, err = m.h.db.SaveNodeRoutes(m.node) - if err != nil { - return err - } - - if m.h.ACLPolicy != nil { - // update routes with peer information - err := m.h.db.EnableAutoApprovedRoutes(m.h.ACLPolicy, m.node) - if err != nil { - return err - } - } - } - - if err := m.h.db.DB.Save(m.node).Error; err != nil { - return err - } - - ctx := types.NotifyCtx(context.Background(), "pre-68-update-while-stream", m.node.Hostname) - m.h.nodeNotifier.NotifyWithIgnore( - ctx, - types.StateUpdate{ - Type: types.StatePeerChanged, - ChangeNodes: []types.NodeID{m.node.ID}, - Message: "called from handlePoll -> pre-68-update-while-stream", - }, - m.node.ID) - - return nil -} - func (m *mapSession) handleReadOnlyRequest() { m.tracef("Client asked for a lite update, responding without peers") diff --git a/integration/scenario.go b/integration/scenario.go index df978f2a..b45c5fe7 100644 --- a/integration/scenario.go +++ b/integration/scenario.go @@ -53,21 +53,23 @@ var ( tailscaleVersions2021 = map[string]bool{ "head": true, "unstable": true, - "1.70": true, // CapVer: not checked - "1.68": true, // CapVer: not checked - "1.66": true, // CapVer: not checked - "1.64": true, // CapVer: not checked - "1.62": true, // CapVer: not checked - "1.60": true, // CapVer: not checked - "1.58": true, // CapVer: not checked - "1.56": true, // CapVer: 82 - "1.54": true, // CapVer: 79 - "1.52": true, // CapVer: 79 - "1.50": true, // CapVer: 74 - "1.48": true, // CapVer: 68 - "1.46": true, // CapVer: 65 + "1.74": true, // CapVer: 106 + "1.72": true, // CapVer: 104 + "1.70": true, // CapVer: 102 + "1.68": true, // CapVer: 97 + "1.66": true, // CapVer: 95 + "1.64": true, // CapVer: 90 + "1.62": true, // CapVer: 88 + "1.60": true, // CapVer: 87 + "1.58": true, // CapVer: 85 + "1.56": true, // Oldest supported version, CapVer: 82 + "1.54": false, // CapVer: 79 + "1.52": false, // CapVer: 79 + "1.50": false, // CapVer: 74 + "1.48": false, // CapVer: 68 + "1.46": false, // CapVer: 65 "1.44": false, // CapVer: 63 - "1.42": false, // Oldest supported version, CapVer: 61 + "1.42": false, // CapVer: 61 "1.40": false, // CapVer: 61 "1.38": false, // CapVer: 58 "1.36": false, // CapVer: 56 From e367454745b2c07eff1601c1988c6cfaf11a0da9 Mon Sep 17 00:00:00 2001 From: nblock Date: Wed, 25 Sep 2024 09:52:28 +0200 Subject: [PATCH 55/60] Add -it to docker exec (#2148) Some commands such as `nodes delete` require user interaction and they fail if `-it` is no supplied to `docker exec`. Use `docker exec -it` in documentation examples to also make them work in interactive commands. --- docs/running-headscale-container.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/running-headscale-container.md b/docs/running-headscale-container.md index 087dae30..4357ab55 100644 --- a/docs/running-headscale-container.md +++ b/docs/running-headscale-container.md @@ -101,7 +101,7 @@ not work with alternatives like [Podman](https://podman.io). The Docker image ca 1. Create a user ([tailnet](https://tailscale.com/kb/1136/tailnet/)): ```shell - docker exec headscale \ + docker exec -it headscale \ headscale users create myfirstuser ``` @@ -116,7 +116,7 @@ tailscale up --login-server YOUR_HEADSCALE_URL To register a machine when running `headscale` in a container, take the headscale command and pass it to the container: ```shell -docker exec headscale \ +docker exec -it headscale \ headscale nodes register --user myfirstuser --key ``` @@ -125,7 +125,7 @@ docker exec headscale \ Generate a key using the command line: ```shell -docker exec headscale \ +docker exec -it headscale \ headscale preauthkeys create --user myfirstuser --reusable --expiration 24h ``` @@ -161,4 +161,4 @@ You can also execute commands directly, such as `ls /ko-app` in this example: docker run headscale/headscale:x.x.x-debug ls /ko-app ``` -Using `docker exec` allows you to run commands in an existing container. +Using `docker exec -it` allows you to run commands in an existing container. From 2c974dd72db83ba133aed50cb10fb1fbc6594699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Thu, 26 Sep 2024 12:09:19 -0700 Subject: [PATCH 56/60] MagicDNS no longer requires nameservers (#1681) According to https://tailscale.com/kb/1081/magicdns#accessing-devices-over-magicdns, > MagicDNS does not require a DNS nameserver if running Tailscale v1.20 or later. --- config-example.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/config-example.yaml b/config-example.yaml index 04a2f342..5b757bc9 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -260,7 +260,6 @@ policy: # 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. From 204a10238990474542efe6c5664faf4a69342cd5 Mon Sep 17 00:00:00 2001 From: Hazel Atkinson <19270622+yellowsink@users.noreply.github.com> Date: Fri, 27 Sep 2024 08:16:18 +0100 Subject: [PATCH 57/60] Add ouroboros to web ui list (#2154) --- docs/web-ui.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/web-ui.md b/docs/web-ui.md index fae71be1..57631845 100644 --- a/docs/web-ui.md +++ b/docs/web-ui.md @@ -5,11 +5,12 @@ This page contains community contributions. The projects listed here are not maintained by the Headscale authors and are written by community members. -| Name | Repository Link | Description | Status | -| --------------- | ------------------------------------------------------- | --------------------------------------------------------------------------- | ------ | -| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha | -| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha | -| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha | -| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for Headscale | Beta | +| Name | Repository Link | Description | Status | +| --------------- | ------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------ | +| headscale-webui | [Github](https://github.com/ifargle/headscale-webui) | A simple Headscale web UI for small-scale deployments. | Alpha | +| headscale-ui | [Github](https://github.com/gurucomputing/headscale-ui) | A web frontend for the headscale Tailscale-compatible coordination server | Alpha | +| HeadscaleUi | [GitHub](https://github.com/simcu/headscale-ui) | A static headscale admin ui, no backend enviroment required | Alpha | +| headscale-admin | [Github](https://github.com/GoodiesHQ/headscale-admin) | Headscale-Admin is meant to be a simple, modern web interface for Headscale | Beta | +| ouroboros | [Github](https://github.com/yellowsink/ouroboros) | Ouroboros is designed for users to manage their own devices, rather than for admins | Stable | You can ask for support on our dedicated [Discord channel](https://discord.com/channels/896711691637780480/1105842846386356294). From 49ce5734fc466714c777acca0eb84e5613a2305b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 29 Sep 2024 08:24:08 +0000 Subject: [PATCH 58/60] flake.lock: Update (#2158) --- flake.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/flake.lock b/flake.lock index 935f2263..d6538314 100644 --- a/flake.lock +++ b/flake.lock @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1726871744, - "narHash": "sha256-V5LpfdHyQkUF7RfOaDPrZDP+oqz88lTJrMT1+stXNwo=", + "lastModified": 1727524699, + "narHash": "sha256-k6YxGj08voz9NvuKExojiGXAVd69M8COtqWSKr6sQS4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a1d92660c6b3b7c26fb883500a80ea9d33321be2", + "rev": "b5b2fecd0cadd82ef107c9583018f381ae70f222", "type": "github" }, "original": { From 5eda9c8d2de5000b6a6b1fc1d73df5c43139f1d3 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Sun, 29 Sep 2024 13:00:27 +0200 Subject: [PATCH 59/60] denormalise PreAuthKey tags (#2155) this commit denormalises the Tags related to a Pre auth key back onto the preauthkey table and struct as a string list. There was not really any real normalisation here as we just added a bunch of duplicate tags with new IDs and preauthkeyIDs, lots of GORM cermony but no actual advantage. This work is the start to fixup tags which currently are not working as they should. Updates #1369 Signed-off-by: Kristoffer Dalby --- hscontrol/db/db.go | 57 +++++++++++++++- hscontrol/db/db_test.go | 64 ++++++++++++++++++ hscontrol/db/preauth_keys.go | 31 +++------ ...3-0-to-0-24-0-preauthkey-tags-table.sqlite | Bin 0 -> 69632 bytes hscontrol/types/preauth_key.go | 19 ++---- 5 files changed, 133 insertions(+), 38 deletions(-) create mode 100644 hscontrol/db/testdata/0-23-0-to-0-24-0-preauthkey-tags-table.sqlite diff --git a/hscontrol/db/db.go b/hscontrol/db/db.go index accf439e..e5a47953 100644 --- a/hscontrol/db/db.go +++ b/hscontrol/db/db.go @@ -3,6 +3,7 @@ package db import ( "context" "database/sql" + "encoding/json" "errors" "fmt" "net/netip" @@ -19,6 +20,7 @@ import ( "gorm.io/driver/postgres" "gorm.io/gorm" "gorm.io/gorm/logger" + "tailscale.com/util/set" ) var errDatabaseNotSupported = errors.New("database type not supported") @@ -291,7 +293,12 @@ func NewHeadscaleDatabase( return err } - err = tx.AutoMigrate(&types.PreAuthKeyACLTag{}) + type preAuthKeyACLTag struct { + ID uint64 `gorm:"primary_key"` + PreAuthKeyID uint64 + Tag string + } + err = tx.AutoMigrate(&preAuthKeyACLTag{}) if err != nil { return err } @@ -413,6 +420,54 @@ func NewHeadscaleDatabase( }, Rollback: func(db *gorm.DB) error { return nil }, }, + // denormalise the ACL tags for preauth keys back onto + // the preauth key table. We dont normalise or reuse and + // it is just a bunch of work for extra work. + { + ID: "202409271400", + Migrate: func(tx *gorm.DB) error { + preauthkeyTags := map[uint64]set.Set[string]{} + + type preAuthKeyACLTag struct { + ID uint64 `gorm:"primary_key"` + PreAuthKeyID uint64 + Tag string + } + + var aclTags []preAuthKeyACLTag + if err := tx.Find(&aclTags).Error; err != nil { + return err + } + + // Store the current tags. + for _, tag := range aclTags { + if preauthkeyTags[tag.PreAuthKeyID] == nil { + preauthkeyTags[tag.PreAuthKeyID] = set.SetOf([]string{tag.Tag}) + } else { + preauthkeyTags[tag.PreAuthKeyID].Add(tag.Tag) + } + } + + // Add tags column and restore the tags. + _ = tx.Migrator().AddColumn(&types.PreAuthKey{}, "tags") + for keyID, tags := range preauthkeyTags { + s := tags.Slice() + j, err := json.Marshal(s) + if err != nil { + return err + } + if err := tx.Model(&types.PreAuthKey{}).Where("id = ?", keyID).Update("tags", string(j)).Error; err != nil { + return err + } + } + + // Drop the old table. + _ = tx.Migrator().DropTable(&preAuthKeyACLTag{}) + + return nil + }, + Rollback: func(db *gorm.DB) error { return nil }, + }, }, ) diff --git a/hscontrol/db/db_test.go b/hscontrol/db/db_test.go index b32d93ce..157ede8b 100644 --- a/hscontrol/db/db_test.go +++ b/hscontrol/db/db_test.go @@ -6,6 +6,8 @@ import ( "net/netip" "os" "path/filepath" + "slices" + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -108,6 +110,68 @@ func TestMigrations(t *testing.T) { } }, }, + // at 14:15:06 ❯ go run ./cmd/headscale preauthkeys list + // ID | Key | Reusable | Ephemeral | Used | Expiration | Created | Tags + // 1 | 09b28f.. | false | false | false | 2024-09-27 | 2024-09-27 | tag:derp + // 2 | 3112b9.. | false | false | false | 2024-09-27 | 2024-09-27 | tag:derp + // 3 | 7c23b9.. | false | false | false | 2024-09-27 | 2024-09-27 | tag:derp,tag:merp + // 4 | f20155.. | false | false | false | 2024-09-27 | 2024-09-27 | tag:test + // 5 | b212b9.. | false | false | false | 2024-09-27 | 2024-09-27 | tag:test,tag:woop,tag:dedu + { + dbPath: "testdata/0-23-0-to-0-24-0-preauthkey-tags-table.sqlite", + wantFunc: func(t *testing.T, h *HSDatabase) { + keys, err := Read(h.DB, func(rx *gorm.DB) ([]types.PreAuthKey, error) { + kratest, err := ListPreAuthKeys(rx, "kratest") + if err != nil { + return nil, err + } + + testkra, err := ListPreAuthKeys(rx, "testkra") + if err != nil { + return nil, err + } + + return append(kratest, testkra...), nil + }) + assert.NoError(t, err) + + assert.Len(t, keys, 5) + want := []types.PreAuthKey{ + { + ID: 1, + Tags: []string{"tag:derp"}, + }, + { + ID: 2, + Tags: []string{"tag:derp"}, + }, + { + ID: 3, + Tags: []string{"tag:derp", "tag:merp"}, + }, + { + ID: 4, + Tags: []string{"tag:test"}, + }, + { + ID: 5, + Tags: []string{"tag:test", "tag:woop", "tag:dedu"}, + }, + } + + if diff := cmp.Diff(want, keys, cmp.Comparer(func(a, b []string) bool { + sort.Sort(sort.StringSlice(a)) + sort.Sort(sort.StringSlice(b)) + return slices.Equal(a, b) + }), cmpopts.IgnoreFields(types.PreAuthKey{}, "Key", "UserID", "User", "CreatedAt", "Expiration")); diff != "" { + t.Errorf("TestMigrations() mismatch (-want +got):\n%s", diff) + } + + if h.DB.Migrator().HasTable("pre_auth_key_acl_tags") { + t.Errorf("TestMigrations() table pre_auth_key_acl_tags should not exist") + } + }, + }, } for _, tt := range tests { diff --git a/hscontrol/db/preauth_keys.go b/hscontrol/db/preauth_keys.go index 5ea59a9c..96420211 100644 --- a/hscontrol/db/preauth_keys.go +++ b/hscontrol/db/preauth_keys.go @@ -11,6 +11,7 @@ import ( "github.com/juanfont/headscale/hscontrol/types" "gorm.io/gorm" "tailscale.com/types/ptr" + "tailscale.com/util/set" ) var ( @@ -47,6 +48,11 @@ func CreatePreAuthKey( return nil, err } + // Remove duplicates + aclTags = set.SetOf(aclTags).Slice() + + // TODO(kradalby): factor out and create a reusable tag validation, + // check if there is one in Tailscale's lib. for _, tag := range aclTags { if !strings.HasPrefix(tag, "tag:") { return nil, fmt.Errorf( @@ -71,28 +77,13 @@ func CreatePreAuthKey( Ephemeral: ephemeral, CreatedAt: &now, Expiration: expiration, + Tags: types.StringList(aclTags), } if err := tx.Save(&key).Error; err != nil { return nil, fmt.Errorf("failed to create key in the database: %w", err) } - if len(aclTags) > 0 { - seenTags := map[string]bool{} - - for _, tag := range aclTags { - if !seenTags[tag] { - if err := tx.Save(&types.PreAuthKeyACLTag{PreAuthKeyID: key.ID, Tag: tag}).Error; err != nil { - return nil, fmt.Errorf( - "failed to create key tag in the database: %w", - err, - ) - } - seenTags[tag] = true - } - } - } - return &key, nil } @@ -110,7 +101,7 @@ func ListPreAuthKeys(tx *gorm.DB, userName string) ([]types.PreAuthKey, error) { } keys := []types.PreAuthKey{} - if err := tx.Preload("User").Preload("ACLTags").Where(&types.PreAuthKey{UserID: user.ID}).Find(&keys).Error; err != nil { + if err := tx.Preload("User").Where(&types.PreAuthKey{UserID: user.ID}).Find(&keys).Error; err != nil { return nil, err } @@ -135,10 +126,6 @@ func GetPreAuthKey(tx *gorm.DB, user string, key string) (*types.PreAuthKey, err // does not exist. func DestroyPreAuthKey(tx *gorm.DB, pak types.PreAuthKey) error { return tx.Transaction(func(db *gorm.DB) error { - if result := db.Unscoped().Where(types.PreAuthKeyACLTag{PreAuthKeyID: pak.ID}).Delete(&types.PreAuthKeyACLTag{}); result.Error != nil { - return result.Error - } - if result := db.Unscoped().Delete(pak); result.Error != nil { return result.Error } @@ -182,7 +169,7 @@ func (hsdb *HSDatabase) ValidatePreAuthKey(k string) (*types.PreAuthKey, error) // If returns no error and a PreAuthKey, it can be used. func ValidatePreAuthKey(tx *gorm.DB, k string) (*types.PreAuthKey, error) { pak := types.PreAuthKey{} - if result := tx.Preload("User").Preload("ACLTags").First(&pak, "key = ?", k); errors.Is( + if result := tx.Preload("User").First(&pak, "key = ?", k); errors.Is( result.Error, gorm.ErrRecordNotFound, ) { diff --git a/hscontrol/db/testdata/0-23-0-to-0-24-0-preauthkey-tags-table.sqlite b/hscontrol/db/testdata/0-23-0-to-0-24-0-preauthkey-tags-table.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..512c487996b18582e26bb214a74258f545a3a379 GIT binary patch literal 69632 zcmeI*J#5?90S9nWAC}~c#JD&>Faj6M1wkytXYiY%ivo2WB`{(scm4n_lSqnBHWBM1 z5}i6%bV%9^ncA`4I<-R&S9IxAAXB%ZxS@LsbSk)OaGSWU;KEVZsekXAJ0bH@oN-J zy=0m7Plsh1#pOO{xL@m`}ek6bl+y9wZCTetj>|!C+!F1vB`Vn)LRQmN`@VG+!-VzNZ%O_ zXxZa9@m}N7krwWQ|rexwdAWjLE2- z=)#&gB8TqSqp>~W9Svyb!p72NrbC>zXB|#jW1VdFfHv=$uNI9L37Iek#$X!GkI(9Vd^?)j}wcJyTEcX~E9(Xa#x z&1r?awI6wOAXcmF^E=al7!0)Z;QJtm`vAl4YY)4jJEHS#IuQToqqcRgwYR^!x!v05 z&F+JCxElDg$mDO|+ih&$X$7-?buzPR{BGlRW4F=TYV7er?3fN!I^}Bq@V(X8Y0$N0 zYjbaF^HyVRZt3wno2?;^_vc@_mf?uB2lE8{`jE5C+20GBibRHlW`>NXjl%NkM(jmAGd-L9_NT(x%7ODqu*dY zMioWN)sZzmGI{%WV6U0;k2yJ|W5e>?L299zvwGE@Ub|GN>N@*F5-ueP+_u#@4%Wn^ z_$0ED?$q2oZ`Q-fQBAM8bXAVl>6rnPta3bQ*X%$_=6d776b;>4cab73vrkHsu{0W- zc&X(*jJOo3BnHtdIm2NTM$atlPY+6k>hd!CEFT<=Fi-gWUfMg_{XCa+e>q4W8fr`D zQT#4yIgiN5bH{|ful7%RWaM_}n`7TU`AEjN{`_-$mcMqz*=CMxxbVbXOfNlgE7FTX zu*Ykcp4!u1p-{bXgMGIA$|xTv2gtaTR!*h8!DvpL2+cbQIT`yX)9#yOctj|ebv%iq zi|6glgPnc8YaNg2Nd_%uia4A228+OqbgF~xvs&tF_ClemR@o={$$asPPM!?~;o_^a zWj0?pS%TP^x$;qdp?dQsdp|g$#hXv)HqRyH`NdHVgJ`XDlR%BQM|-V)DsmQK%%lgIj` z??yccPn4rKyHzs?)@pKQp?c@)>-yev55u<;3NYQyeZnqOuU=(;5ExA3r(4;y#xxBujMlpN%3Wm;V_qd%Z*pi6TVJ|NKA0 z*$np^`h*7rAOHafKmY;|fB*y_009U<00RG|Kr^$zG@Hy7|I+Hu@)o%wqORDZ2hIb1#@2_Zh?ellwdO8TZA- z?j%|d0SG_<0uX=z1Rwwb2tWV=5V-gPOF8-<{!>3Bk*A$nDkYN$P_c#YBBgJkMQ&VmkjqW?&ZZ7LJuGS0SG_<0uX=z1Rwwb2tWV=5J(8*vJ9Ka zW|>m({r}~kG2An5vGSA3`*afz2tWV=5P$##AOHafKmY;|I8TAc`4ZD(i^XE|t}Tg@ zZ5V>6sibb`hN3u*tm~3q*HlGO1WA{St}QEuW2r({)icRk$)ycZ-4OhH!wo^uZVJN8 zt>*G1T@#Hk`O{onb+b$FCs)L^{;>+~*oziFpgT6c+d`)*V3NV08o=~vkqnn-j_FjPsmNLR1h z-MZAZMMKaOo8CC@==F}Q(WXvsu&2G2H)LtuFbrvar=w)OuGGVtA7`SPn@yR1nZ9kP za)+WSq9NLnqgtdR+I2&eRDu54UENk>omd^!k+hkbr43bGmo-&3=2so1%W91Bn4MHD z7!;00bZa0SG_<0uX=z1Rwx`^A^DM|9Oj}J_tYn0uX=z1Rwwb z2tWV=5P-n>3;6f{+Q00Izz z00bZa0SG_<0uX?}+Y#{p|9_Et&TwCHf93wf{hs?R_e<`lGy)F@KmY;|fB*y_009U< z00Izz00honpj^x`>`LCV4mTX)oRkZb;P(cDVR>N^^vKvN=lx*L&m07EelY6?dt@{$ zXD30&4`%#eCM}xKmY;|fB*y_009U<00Izzz}XAn{{OQ#MTroA00bZa0SG_<0uX=z1Rwx`cTM2m DQpukF literal 0 HcmV?d00001 diff --git a/hscontrol/types/preauth_key.go b/hscontrol/types/preauth_key.go index 8b02569a..ba3b597b 100644 --- a/hscontrol/types/preauth_key.go +++ b/hscontrol/types/preauth_key.go @@ -16,21 +16,14 @@ type PreAuthKey struct { UserID uint User User `gorm:"constraint:OnDelete:CASCADE;"` Reusable bool - Ephemeral bool `gorm:"default:false"` - Used bool `gorm:"default:false"` - ACLTags []PreAuthKeyACLTag `gorm:"constraint:OnDelete:CASCADE;"` + Ephemeral bool `gorm:"default:false"` + Used bool `gorm:"default:false"` + Tags []string `gorm:"serializer:json"` CreatedAt *time.Time Expiration *time.Time } -// PreAuthKeyACLTag describes an autmatic tag applied to a node when registered with the associated PreAuthKey. -type PreAuthKeyACLTag struct { - ID uint64 `gorm:"primary_key"` - PreAuthKeyID uint64 - Tag string -} - func (key *PreAuthKey) Proto() *v1.PreAuthKey { protoKey := v1.PreAuthKey{ User: key.User.Name, @@ -39,7 +32,7 @@ func (key *PreAuthKey) Proto() *v1.PreAuthKey { Ephemeral: key.Ephemeral, Reusable: key.Reusable, Used: key.Used, - AclTags: make([]string, len(key.ACLTags)), + AclTags: key.Tags, } if key.Expiration != nil { @@ -50,9 +43,5 @@ func (key *PreAuthKey) Proto() *v1.PreAuthKey { protoKey.CreatedAt = timestamppb.New(*key.CreatedAt) } - for idx := range key.ACLTags { - protoKey.AclTags[idx] = key.ACLTags[idx].Tag - } - return &protoKey } From 63035cdb5af1d8a652ecd7904714ff385ed25689 Mon Sep 17 00:00:00 2001 From: Jacob Yundt Date: Sun, 29 Sep 2024 07:00:52 -0400 Subject: [PATCH 60/60] Update headscale user creation settings in .deb (#2134) * Update headscale user creation settings in .deb Update the headscale user settings to: - shell = /usr/sbin/nologin - home-dir = /var/lib/headscale This syncs the .deb installation behavior with the current Linux docs: https://github.com/juanfont/headscale/blob/fe68f503289db6cb1c2a568b8ae02a45ac632dd6/docs/running-headscale-linux-manual.md?plain=1#L39-L45 Fixes juanfont/headscale#2133 * slight refactor to use existing variables. * Fixup for HOME_DIR var --- docs/packaging/postinstall.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/packaging/postinstall.sh b/docs/packaging/postinstall.sh index 7d934a9a..2bc89703 100644 --- a/docs/packaging/postinstall.sh +++ b/docs/packaging/postinstall.sh @@ -6,8 +6,10 @@ HEADSCALE_EXE="/usr/bin/headscale" BSD_HIER="" HEADSCALE_RUN_DIR="/var/run/headscale" +HEADSCALE_HOME_DIR="/var/lib/headscale" HEADSCALE_USER="headscale" HEADSCALE_GROUP="headscale" +HEADSCALE_SHELL="/usr/sbin/nologin" ensure_sudo() { if [ "$(id -u)" = "0" ]; then @@ -29,7 +31,7 @@ ensure_headscale_path() { create_headscale_user() { printf "PostInstall: Adding headscale user %s\n" "$HEADSCALE_USER" - useradd -s /bin/sh -c "headscale default user" headscale + useradd -s "$HEADSCALE_SHELL" -d "$HEADSCALE_HOME_DIR" -c "headscale default user" "$HEADSCALE_USER" } create_headscale_group() {