feat: make DNS TLD configurable (#124)

Add a 'domain' option to the NixOS module and JSON config that
controls the domain suffix used for internal DNS names. Defaults to
'hyprspace' for backward compatibility. Supports multi-label domains
(e.g. 'vpn.internal').

Co-authored-by: Max <max@privatevoid.net>
This commit is contained in:
wanderer 2026-06-22 19:48:14 +02:00 committed by GitHub
parent 6f3c85a1ef
commit f1538eef4d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 64 additions and 12 deletions

View File

@ -30,6 +30,7 @@ type Config struct {
BuiltinAddr6 net.IP `json:"-"`
Services map[string]Service `json:"-"`
FilterPrivateAddresses bool `json:"-"`
Domain string `json:"-"`
}
// Peer defines a peer in the configuration. We might add more to this later.
@ -196,6 +197,11 @@ func Read(path string) (*Config, error) {
result.FilterPrivateAddresses = input.FilterPrivateAddresses
result.Domain = input.Domain
if result.Domain == "" {
result.Domain = "hyprspace"
}
for _, addrString := range input.BootstrapPeers {
addr, err := multiaddr.NewMultiaddr(addrString)
if err != nil {

View File

@ -22,7 +22,7 @@ func Test_domainSuffix(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.iface, func(t *testing.T) {
assert.Equal(t, tt.want, domainSuffix(config.Config{Interface: tt.iface}))
assert.Equal(t, tt.want, domainSuffix(config.Config{Interface: tt.iface, Domain: "hyprspace"}))
})
}
}
@ -37,13 +37,13 @@ func Test_withDomainSuffix(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.iface, func(t *testing.T) {
assert.Equal(t, tt.want, withDomainSuffix(config.Config{Interface: tt.iface}, tt.prefix))
assert.Equal(t, tt.want, withDomainSuffix(config.Config{Interface: tt.iface, Domain: "hyprspace"}, tt.prefix))
})
}
}
func Test_mkAliasRecord_emptyService(t *testing.T) {
cfg := config.Config{Interface: "hs0"}
cfg := config.Config{Interface: "hs0", Domain: "hyprspace"}
pk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(pk)
@ -60,7 +60,7 @@ func Test_mkAliasRecord_emptyService(t *testing.T) {
}
func Test_mkAliasRecord_withService(t *testing.T) {
cfg := config.Config{Interface: "hs0"}
cfg := config.Config{Interface: "hs0", Domain: "hyprspace"}
pk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(pk)
@ -74,7 +74,7 @@ func Test_mkAliasRecord_withService(t *testing.T) {
}
func Test_mkAliasRecord_emptyName(t *testing.T) {
cfg := config.Config{Interface: "hs0"}
cfg := config.Config{Interface: "hs0", Domain: "hyprspace"}
pk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(pk)
@ -134,7 +134,7 @@ func Test_mkIDRecord(t *testing.T) {
for i, tt := range tests {
name := []string{"ipv4", "ipv6", "ipv6-svc"}[i]
t.Run(name, func(t *testing.T) {
cfg := config.Config{Interface: "hs0"}
cfg := config.Config{Interface: "hs0", Domain: "hyprspace"}
record := tt.fn(cfg, pid, "", tt.addr)
hdr := record.Header()
@ -160,7 +160,7 @@ func Test_mkIDRecord(t *testing.T) {
}
func Test_mkIDRecord_nil_addr(t *testing.T) {
cfg := config.Config{Interface: "hs0"}
cfg := config.Config{Interface: "hs0", Domain: "hyprspace"}
pk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 256)
require.NoError(t, err)
pid, err := peer.IDFromPrivateKey(pk)

View File

@ -21,11 +21,11 @@ import (
var logger = log.Logger("hyprspace/dns")
func domainSuffix(config config.Config) string {
domain := config.Domain
if config.Interface == "hyprspace" {
return "hyprspace."
} else {
return fmt.Sprintf("%s.hyprspace.", config.Interface)
return domain + "."
}
return fmt.Sprintf("%s.%s.", config.Interface, domain)
}
func withDomainSuffix(config config.Config, str string) string {

33
dns/server_test.go Normal file
View File

@ -0,0 +1,33 @@
package dns
import (
"testing"
"github.com/hyprspace/hyprspace/config"
)
func TestDomainSuffix(t *testing.T) {
tests := []struct {
name string
iface string
domain string
want string
}{
{"default iface, default domain", "hyprspace", "hyprspace", "hyprspace."},
{"default iface, custom domain", "hyprspace", "vpn", "vpn."},
{"custom iface, default domain", "hs0", "hyprspace", "hs0.hyprspace."},
{"custom iface, custom domain", "hs0", "vpn", "hs0.vpn."},
{"multi-label domain", "hyprspace", "vpn.internal", "vpn.internal."},
{"custom iface, multi-label domain", "hs0", "vpn.internal", "hs0.vpn.internal."},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := config.Config{Interface: tt.iface, Domain: tt.domain}
got := domainSuffix(cfg)
if got != tt.want {
t.Errorf("domainSuffix() = %q, want %q", got, tt.want)
}
})
}
}

View File

@ -4,7 +4,11 @@ Hyprspace runs a simple DNS server on the loopback interface for the purpose of
## Domain
All DNS records are placed under a specific domain, based on the name of the network interface that Hyprspace controls. If the interface name is `hyprspace`, then the top-level domain `hyprspace.` is used. Otherwise, the domain `${interfaceName}.hyprspace.` is used. The rest of this document will assume that the domain is `hyprspace.`.
All DNS records are placed under a specific domain, based on the `domain` config key (which defaults to `hyprspace`). If the interface name is `hyprspace` and the default `domain` is used, then the domain `hyprspace.` is used. Otherwise, the domain `${interfaceName}.${domain}.` is used. The rest of this document will assume that the domain is `hyprspace.`.
## `domain` option
The `domain` option in the JSON config file or the NixOS module controls the domain used for the internal DNS zone. The default is `hyprspace`. This may be a simple label (e.g. `vpn`) or a multi-label domain (e.g. `vpn.internal`). For example, setting `"domain": "vpn.internal"` would make all DNS names end in `.vpn.internal.` instead of `.hyprspace.`.
## Node lookup

View File

@ -24,7 +24,9 @@ The config key `services` is used to configure the services a particular node sh
}
}
```
This will make the services `example.mynode.hyprspace` and `other.mynode.hyrpspace` available. Upon connecting to the `example` service, the node establishes a connection to `/ip4/127.0.0.1/tcp/8080` and will relay all traffic between the service and the client.
This will make the services `example.mynode.hyprspace` and `other.mynode.hyprspace` available. Upon connecting to the `example` service, the node establishes a connection to `/ip4/127.0.0.1/tcp/8080` and will relay all traffic between the service and the client.
> **Note:** The `.hyprspace.` suffix follows the `domain` configuration option. If you change it (e.g. to `vpn.internal`), the hostnames will use that domain instead (e.g. `example.mynode.vpn.internal`).
## Implementation details

View File

@ -83,6 +83,13 @@ in
options = {
filterPrivateAddresses = mkEnableOption "filtering of private/link-local addresses from peer discovery. When enabled, the node will not attempt to connect to RFC1918, link-local, or loopback addresses advertised by other peers";
domain = mkOption {
type = types.str;
description = "Domain suffix used for DNS names within the Hyprspace network.";
default = "hyprspace";
example = "vpn.internal";
};
bootstrapPeers = mkOption {
type = types.listOf t.multiAddr;
description = "List of libp2p bootstrap node multiaddresses for initial network discovery.";