diff --git a/config/config.go b/config/config.go index 7176c38..2fea248 100644 --- a/config/config.go +++ b/config/config.go @@ -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 { diff --git a/dns/dns_response_test.go b/dns/dns_response_test.go index ef38e8c..c589591 100644 --- a/dns/dns_response_test.go +++ b/dns/dns_response_test.go @@ -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) diff --git a/dns/server.go b/dns/server.go index b97228e..20394ca 100644 --- a/dns/server.go +++ b/dns/server.go @@ -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 { diff --git a/dns/server_test.go b/dns/server_test.go new file mode 100644 index 0000000..97edc46 --- /dev/null +++ b/dns/server_test.go @@ -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) + } + }) + } +} diff --git a/docs/content/features/dns.md b/docs/content/features/dns.md index 1c93e63..88891d8 100644 --- a/docs/content/features/dns.md +++ b/docs/content/features/dns.md @@ -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 diff --git a/docs/content/features/service-network.md b/docs/content/features/service-network.md index b92df6d..326612a 100644 --- a/docs/content/features/service-network.md +++ b/docs/content/features/service-network.md @@ -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 diff --git a/nixos/settings.nix b/nixos/settings.nix index 8bd38af..e7a9ba3 100644 --- a/nixos/settings.nix +++ b/nixos/settings.nix @@ -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.";