diff --git a/nixos/tests/headscale.nix b/nixos/tests/headscale.nix index cb5ce26b7944e3..3ba331b7c3d4b4 100644 --- a/nixos/tests/headscale.nix +++ b/nixos/tests/headscale.nix @@ -13,7 +13,10 @@ import ./make-test-python.nix ( in { name = "headscale"; - meta.maintainers = with lib.maintainers; [ misterio77 ]; + meta.maintainers = with lib.maintainers; [ + kradalby + misterio77 + ]; nodes = let diff --git a/pkgs/servers/headscale/default.nix b/pkgs/servers/headscale/default.nix index 1287d3668ea998..8cc608dc228cbc 100644 --- a/pkgs/servers/headscale/default.nix +++ b/pkgs/servers/headscale/default.nix @@ -4,26 +4,33 @@ fetchFromGitHub, installShellFiles, nixosTests, + postgresql, }: buildGoModule rec { pname = "headscale"; - version = "0.23.0"; + version = "0.24.0"; src = fetchFromGitHub { owner = "juanfont"; repo = "headscale"; rev = "v${version}"; - hash = "sha256-5tlnVNpn+hJayxHjTpbOO3kRInOYOFz0pe9pwjXZlBE="; + hash = "sha256-s9zzhN8NTC6YxOO6fyO+A0jleeY8bhN1wcbf4pvGkpI="; }; - # Merged post-v0.23.0, so should be removed with next release. - patches = [ ./patches/config-loosen-up-BaseDomain-and-ServerURL-checks.patch ]; + vendorHash = "sha256-SBfeixT8DQOrK2SWmHHSOBtzRdSZs+pwomHpw6Jd+qc="; - vendorHash = "sha256-+8dOxPG/Q+wuHgRwwWqdphHOuop0W9dVyClyQuh7aRc="; + subPackages = [ "cmd/headscale" ]; - ldflags = ["-s" "-w" "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}"]; + ldflags = [ + "-s" + "-w" + "-X github.com/juanfont/headscale/cmd/headscale/cli.Version=v${version}" + ]; + + nativeBuildInputs = [ installShellFiles ]; + + nativeCheckInputs = [ postgresql ]; - nativeBuildInputs = [installShellFiles]; checkFlags = ["-short"]; postInstall = '' @@ -33,7 +40,7 @@ buildGoModule rec { --zsh <($out/bin/headscale completion zsh) ''; - passthru.tests = {inherit (nixosTests) headscale;}; + passthru.tests = { inherit (nixosTests) headscale; }; meta = with lib; { homepage = "https://github.com/juanfont/headscale"; @@ -56,6 +63,9 @@ buildGoModule rec { ''; license = licenses.bsd3; mainProgram = "headscale"; - maintainers = with maintainers; [nkje jk kradalby misterio77 ghuntley]; + maintainers = with maintainers; [ + kradalby + misterio77 + ]; }; } diff --git a/pkgs/servers/headscale/patches/config-loosen-up-BaseDomain-and-ServerURL-checks.patch b/pkgs/servers/headscale/patches/config-loosen-up-BaseDomain-and-ServerURL-checks.patch deleted file mode 100644 index d226bc99ceffee..00000000000000 --- a/pkgs/servers/headscale/patches/config-loosen-up-BaseDomain-and-ServerURL-checks.patch +++ /dev/null @@ -1,204 +0,0 @@ -From 6ba8990b0b982b261b0b549080a2f7f780cc70d6 Mon Sep 17 00:00:00 2001 -From: =?UTF-8?q?Motiejus=20Jak=C5=A1tys?= -Date: Thu, 21 Nov 2024 06:28:45 +0200 -Subject: [PATCH] config: loosen up BaseDomain and ServerURL checks - -Requirements [here][1]: - -> OK: -> server_url: headscale.com, base: clients.headscale.com -> server_url: headscale.com, base: headscale.net -> -> Not OK: -> server_url: server.headscale.com, base: headscale.com -> -> Essentially we have to prevent the possibility where the headscale -> server has a URL which can also be assigned to a node. -> -> So for the Not OK scenario: -> -> if the server is: server.headscale.com, and a node joins with the name -> server, it will be assigned server.headscale.com and that will break -> the connection for nodes which will now try to connect to that node -> instead of the headscale server. - -Fixes #2210 - -[1]: https://github.com/juanfont/headscale/issues/2210#issuecomment-2488165187 ---- - hscontrol/types/config.go | 44 +++++++++++-- - hscontrol/types/config_test.go | 64 ++++++++++++++++++- - .../testdata/base-domain-in-server-url.yaml | 2 +- - 3 files changed, 102 insertions(+), 8 deletions(-) - -diff --git a/hscontrol/types/config.go b/hscontrol/types/config.go -index 50ce2f075f4c..b10118aaeade 100644 ---- a/hscontrol/types/config.go -+++ b/hscontrol/types/config.go -@@ -28,8 +28,9 @@ const ( - maxDuration time.Duration = 1<<63 - 1 - ) - --var errOidcMutuallyExclusive = errors.New( -- "oidc_client_secret and oidc_client_secret_path are mutually exclusive", -+var ( -+ errOidcMutuallyExclusive = errors.New("oidc_client_secret and oidc_client_secret_path are mutually exclusive") -+ errServerURLSuffix = errors.New("server_url cannot be part of base_domain in a way that could make the DERP and headscale server unreachable") - ) - - type IPAllocationStrategy string -@@ -814,10 +815,10 @@ func LoadServerConfig() (*Config, error) { - // - 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.") -+ if !dnsConfig.UserNameInMagicDNS && dnsConfig.BaseDomain != "" { -+ if err := isSafeServerURL(serverURL, dnsConfig.BaseDomain); err != nil { -+ return nil, err -+ } - } - - return &Config{ -@@ -910,6 +911,37 @@ func LoadServerConfig() (*Config, error) { - }, nil - } - -+// BaseDomain cannot be a suffix of 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). -+func isSafeServerURL(serverURL, baseDomain string) error { -+ server, err := url.Parse(serverURL) -+ if err != nil { -+ return err -+ } -+ -+ serverDomainParts := strings.Split(server.Host, ".") -+ baseDomainParts := strings.Split(baseDomain, ".") -+ -+ if len(serverDomainParts) <= len(baseDomainParts) { -+ return nil -+ } -+ -+ s := len(serverDomainParts) -+ b := len(baseDomainParts) -+ for i := range len(baseDomainParts) { -+ if serverDomainParts[s-i-1] != baseDomainParts[b-i-1] { -+ return nil -+ } -+ } -+ -+ return errServerURLSuffix -+} -+ - 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 e6e8d6c2e0b1..68a13f6c0f40 100644 ---- a/hscontrol/types/config_test.go -+++ b/hscontrol/types/config_test.go -@@ -1,6 +1,7 @@ - package types - - import ( -+ "fmt" - "os" - "path/filepath" - "testing" -@@ -141,7 +142,7 @@ func TestReadConfig(t *testing.T) { - 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.", -+ wantErr: errServerURLSuffix.Error(), - }, - { - name: "base-domain-not-in-server-url", -@@ -337,3 +338,64 @@ tls_letsencrypt_challenge_type: TLS-ALPN-01 - err = LoadConfig(tmpDir, false) - assert.NoError(t, err) - } -+ -+// OK -+// server_url: headscale.com, base: clients.headscale.com -+// server_url: headscale.com, base: headscale.net -+// -+// NOT OK -+// server_url: server.headscale.com, base: headscale.com. -+func TestSafeServerURL(t *testing.T) { -+ tests := []struct { -+ serverURL, baseDomain, -+ wantErr string -+ }{ -+ { -+ serverURL: "https://example.com", -+ baseDomain: "example.org", -+ }, -+ { -+ serverURL: "https://headscale.com", -+ baseDomain: "headscale.com", -+ }, -+ { -+ serverURL: "https://headscale.com", -+ baseDomain: "clients.headscale.com", -+ }, -+ { -+ serverURL: "https://headscale.com", -+ baseDomain: "clients.subdomain.headscale.com", -+ }, -+ { -+ serverURL: "https://headscale.kristoffer.com", -+ baseDomain: "mybase", -+ }, -+ { -+ serverURL: "https://server.headscale.com", -+ baseDomain: "headscale.com", -+ wantErr: errServerURLSuffix.Error(), -+ }, -+ { -+ serverURL: "https://server.subdomain.headscale.com", -+ baseDomain: "headscale.com", -+ wantErr: errServerURLSuffix.Error(), -+ }, -+ { -+ serverURL: "http://foo\x00", -+ wantErr: `parse "http://foo\x00": net/url: invalid control character in URL`, -+ }, -+ } -+ -+ for _, tt := range tests { -+ testName := fmt.Sprintf("server=%s domain=%s", tt.serverURL, tt.baseDomain) -+ t.Run(testName, func(t *testing.T) { -+ err := isSafeServerURL(tt.serverURL, tt.baseDomain) -+ if tt.wantErr != "" { -+ assert.EqualError(t, err, tt.wantErr) -+ -+ return -+ } -+ assert.NoError(t, err) -+ }) -+ } -+} -diff --git a/hscontrol/types/testdata/base-domain-in-server-url.yaml b/hscontrol/types/testdata/base-domain-in-server-url.yaml -index 683e021837c9..2d6a4694a09a 100644 ---- a/hscontrol/types/testdata/base-domain-in-server-url.yaml -+++ b/hscontrol/types/testdata/base-domain-in-server-url.yaml -@@ -8,7 +8,7 @@ prefixes: - database: - type: sqlite3 - --server_url: "https://derp.no" -+server_url: "https://server.derp.no" - - dns: - magic_dns: true --- -2.47.0 -