Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

config: loosen up BaseDomain and ServerURL checks #2248

Merged
merged 4 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- Added conversion of 'Hostname' to 'givenName' in a node with FQDN rules applied [#2198](https://github.com/juanfont/headscale/pull/2198)
- Fixed updating of hostname and givenName when it is updated in HostInfo [#2199](https://github.com/juanfont/headscale/pull/2199)
- Fixed missing `stable-debug` container tag [#2232](https://github.com/juanfont/headscale/pr/2232)
- Loosened up `server_url` and `base_domain` check. It was overly strict in some cases.

## 0.23.0 (2024-09-18)

Expand Down
45 changes: 38 additions & 7 deletions hscontrol/types/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 a suffix of the base_domain, this will cause the headscale server and embedded DERP to become unreachable from the Tailscale node.")
)

type IPAllocationStrategy string
Expand Down Expand Up @@ -827,11 +828,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)
if 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.BaseDomain != "" {
if err := isSafeServerURL(serverURL, dnsConfig.BaseDomain); err != nil {
return nil, err
}
}

return &Config{
Expand Down Expand Up @@ -924,6 +924,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 := 1; i < len(baseDomainParts)-1; i++ {
if serverDomainParts[s-i] != baseDomainParts[b-i] {
return nil
}
}

return errServerURLSuffix
}

type deprecator struct {
warns set.Set[string]
fatals set.Set[string]
Expand Down
59 changes: 58 additions & 1 deletion hscontrol/types/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package types

import (
"fmt"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -139,7 +140,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: "server_url cannot be a suffix of 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",
Expand Down Expand Up @@ -333,3 +334,59 @@ 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is said elsewhere that server url must not be a suffix of the base_domain. To me, server is a suffix of base here -- yes, additionally they are equal. Perhaps the wording using "suffix" needs to be adjusted?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are write, it should probably be reformulated since we allow different domains, open to proposals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See updated error message. I think the new one is more intuitive, see if you like it better

//
// 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://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)
})
}
}
2 changes: 1 addition & 1 deletion hscontrol/types/testdata/base-domain-in-server-url.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ prefixes:
database:
type: sqlite3

server_url: "https://derp.no"
server_url: "https://server.derp.no"

dns:
magic_dns: true
Expand Down