Add coredns module
This commit is contained in:
68
clanServices/coredns/README.md
Normal file
68
clanServices/coredns/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
This module enables hosting clan-internal services easily, which can be resolved
|
||||
inside your VPN. This allows defining a custom top-level domain (e.g. `.clan`)
|
||||
and exposing endpoints from a machine to others, which will be
|
||||
accessible under `http://<service>.clan` in your browser.
|
||||
|
||||
The service consists of two roles:
|
||||
|
||||
- A `server` role: This is the DNS-server that will be queried when trying to
|
||||
resolve clan-internal services. It defines the top-level domain.
|
||||
- A `default` role: This does two things. First, it sets up the nameservers so
|
||||
thatclan-internal queries are resolved via the `server` machine, while
|
||||
external queries are resolved as normal via DHCP. Second, it allows exposing
|
||||
services (see example below).
|
||||
|
||||
## Example Usage
|
||||
|
||||
Here the machine `dnsserver` is designated as internal DNS-server for the TLD
|
||||
`.foo`. `server01` will host an application that shall be reachable at
|
||||
`http://one.foo` and `server02` is going to be reachable at `http://two.foo`.
|
||||
`client` is any other machine that is part of the clan but does not host any
|
||||
services.
|
||||
|
||||
When `client` tries to resolve `http://one.foo`, the DNS query will be
|
||||
routed to `dnsserver`, which will answer with `192.168.1.3`. If it tries to
|
||||
resolve some external domain (e.g. `https://clan.lol`), the query will not be
|
||||
routed to `dnsserver` but resolved as before, via the nameservers advertised by
|
||||
DHCP.
|
||||
|
||||
```nix
|
||||
inventory = {
|
||||
|
||||
machines = {
|
||||
dnsserver = { }; # 192.168.1.2
|
||||
server01 = { }; # 192.168.1.3
|
||||
server02 = { }; # 192.168.1.4
|
||||
client = { }; # 192.168.1.5
|
||||
};
|
||||
|
||||
instances = {
|
||||
coredns = {
|
||||
|
||||
module.name = "@clan/coredns";
|
||||
module.input = "self";
|
||||
|
||||
# Add the default role to all machines, including `client`
|
||||
roles.default.tags.all = { };
|
||||
|
||||
# DNS server
|
||||
roles.server.machines."dnsserver".settings = {
|
||||
ip = "192.168.1.2";
|
||||
tld = "foo";
|
||||
};
|
||||
|
||||
# First service
|
||||
roles.default.machines."server01".settings = {
|
||||
ip = "192.168.1.3";
|
||||
services = [ "one" ];
|
||||
};
|
||||
|
||||
# Second service
|
||||
roles.default.machines."server02".settings = {
|
||||
ip = "192.168.1.4";
|
||||
services = [ "two" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
157
clanServices/coredns/default.nix
Normal file
157
clanServices/coredns/default.nix
Normal file
@@ -0,0 +1,157 @@
|
||||
{ ... }:
|
||||
{
|
||||
_class = "clan.service";
|
||||
manifest.name = "coredns";
|
||||
manifest.description = "Clan-internal DNS and service exposure";
|
||||
manifest.categories = [ "Network" ];
|
||||
manifest.readme = builtins.readFile ./README.md;
|
||||
|
||||
roles.server = {
|
||||
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.tld = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "clan";
|
||||
description = ''
|
||||
Top-level domain for this instance. All services below this will be
|
||||
resolved internally.
|
||||
'';
|
||||
};
|
||||
|
||||
options.ip = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
# TODO: Set a default
|
||||
description = "IP for the DNS to listen on";
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{
|
||||
roles,
|
||||
settings,
|
||||
...
|
||||
}:
|
||||
{
|
||||
nixosModule =
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
{
|
||||
|
||||
networking.firewall.allowedTCPPorts = [ 53 ];
|
||||
networking.firewall.allowedUDPPorts = [ 53 ];
|
||||
|
||||
services.coredns =
|
||||
let
|
||||
|
||||
# Get all service entries for one host
|
||||
hostServiceEntries =
|
||||
host:
|
||||
lib.strings.concatStringsSep "\n" (
|
||||
map (
|
||||
service: "${service} IN A ${roles.default.machines.${host}.settings.ip} ; ${host}"
|
||||
) roles.default.machines.${host}.settings.services
|
||||
);
|
||||
|
||||
zonefile = pkgs.writeTextFile {
|
||||
name = "db.${settings.tld}";
|
||||
text = ''
|
||||
$TTL 3600
|
||||
@ IN SOA ns.${settings.tld}. admin.${settings.tld}. 1 7200 3600 1209600 3600
|
||||
IN NS ns.${settings.tld}.
|
||||
ns IN A ${settings.ip} ; DNS server
|
||||
|
||||
''
|
||||
+ (lib.strings.concatStringsSep "\n" (
|
||||
map (host: hostServiceEntries host) (lib.attrNames roles.default.machines)
|
||||
));
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
enable = true;
|
||||
config = ''
|
||||
. {
|
||||
forward . 1.1.1.1
|
||||
cache 30
|
||||
}
|
||||
|
||||
${settings.tld} {
|
||||
file ${zonefile}
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
roles.default = {
|
||||
interface =
|
||||
{ lib, ... }:
|
||||
{
|
||||
options.services = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Service endpoints this host exposes (without TLD). Each entry will
|
||||
be resolved to <entry>.<tld> using the configured top-level domain.
|
||||
'';
|
||||
};
|
||||
|
||||
options.ip = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
# TODO: Set a default
|
||||
description = "IP on which the services will listen";
|
||||
};
|
||||
};
|
||||
|
||||
perInstance =
|
||||
{ roles, ... }:
|
||||
{
|
||||
nixosModule =
|
||||
{ lib, ... }:
|
||||
{
|
||||
|
||||
networking.nameservers = map (m: "127.0.0.1:5353#${roles.server.machines.${m}.settings.tld}") (
|
||||
lib.attrNames roles.server.machines
|
||||
);
|
||||
|
||||
services.resolved.domains = map (m: "~${roles.server.machines.${m}.settings.tld}") (
|
||||
lib.attrNames roles.server.machines
|
||||
);
|
||||
|
||||
services.unbound = {
|
||||
enable = true;
|
||||
settings = {
|
||||
server = {
|
||||
port = 5353;
|
||||
verbosity = 2;
|
||||
interface = [ "127.0.0.1" ];
|
||||
access-control = [ "127.0.0.0/8 allow" ];
|
||||
do-not-query-localhost = "no";
|
||||
domain-insecure = map (m: "${roles.server.machines.${m}.settings.tld}.") (
|
||||
lib.attrNames roles.server.machines
|
||||
);
|
||||
};
|
||||
|
||||
# Default: forward everything else to DHCP-provided resolvers
|
||||
forward-zone = [
|
||||
{
|
||||
name = ".";
|
||||
forward-addr = "127.0.0.53@53"; # Forward to systemd-resolved
|
||||
}
|
||||
];
|
||||
stub-zone = map (m: {
|
||||
name = "${roles.server.machines.${m}.settings.tld}.";
|
||||
stub-addr = "${roles.server.machines.${m}.settings.ip}";
|
||||
}) (lib.attrNames roles.server.machines);
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
18
clanServices/coredns/flake-module.nix
Normal file
18
clanServices/coredns/flake-module.nix
Normal file
@@ -0,0 +1,18 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
module = lib.modules.importApply ./default.nix { };
|
||||
in
|
||||
{
|
||||
clan.modules = {
|
||||
coredns = module;
|
||||
};
|
||||
perSystem =
|
||||
{ ... }:
|
||||
{
|
||||
clan.nixosTests.coredns = {
|
||||
imports = [ ./tests/vm/default.nix ];
|
||||
|
||||
clan.modules."@clan/coredns" = module;
|
||||
};
|
||||
};
|
||||
}
|
||||
113
clanServices/coredns/tests/vm/default.nix
Normal file
113
clanServices/coredns/tests/vm/default.nix
Normal file
@@ -0,0 +1,113 @@
|
||||
{
|
||||
...
|
||||
}:
|
||||
{
|
||||
name = "coredns";
|
||||
|
||||
clan = {
|
||||
directory = ./.;
|
||||
test.useContainers = true;
|
||||
inventory = {
|
||||
|
||||
machines = {
|
||||
dns = { }; # 192.168.1.2
|
||||
server01 = { }; # 192.168.1.3
|
||||
server02 = { }; # 192.168.1.4
|
||||
client = { }; # 192.168.1.1
|
||||
};
|
||||
|
||||
instances = {
|
||||
coredns = {
|
||||
|
||||
module.name = "@clan/coredns";
|
||||
module.input = "self";
|
||||
|
||||
roles.default.tags.all = { };
|
||||
|
||||
# First service
|
||||
roles.default.machines."server01".settings = {
|
||||
ip = "192.168.1.3";
|
||||
services = [ "one" ];
|
||||
};
|
||||
|
||||
# Second service
|
||||
roles.default.machines."server02".settings = {
|
||||
ip = "192.168.1.4";
|
||||
services = [ "two" ];
|
||||
};
|
||||
|
||||
# DNS server
|
||||
roles.server.machines."dns".settings = {
|
||||
ip = "192.168.1.2";
|
||||
tld = "foo";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
nodes = {
|
||||
dns =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.net-tools ];
|
||||
};
|
||||
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [ pkgs.net-tools ];
|
||||
};
|
||||
|
||||
server01 = {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."one.foo" = {
|
||||
locations."/" = {
|
||||
return = "200 'test server response one'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
server02 = {
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts."two.foo" = {
|
||||
locations."/" = {
|
||||
return = "200 'test server response two'";
|
||||
extraConfig = "add_header Content-Type text/plain;";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import json
|
||||
start_all()
|
||||
|
||||
machines = [server01, server02, dns, client]
|
||||
|
||||
for m in machines:
|
||||
m.systemctl("start network-online.target")
|
||||
|
||||
for m in machines:
|
||||
m.wait_for_unit("network-online.target")
|
||||
|
||||
# import time
|
||||
# time.sleep(2333333)
|
||||
|
||||
# This should work, but is borken in tests i think? Instead we dig directly
|
||||
|
||||
# client.succeed("curl -k -v http://one.foo")
|
||||
# client.succeed("curl -k -v http://two.foo")
|
||||
|
||||
answer = client.succeed("dig @192.168.1.2 one.foo")
|
||||
assert "192.168.1.3" in answer, "IP not found"
|
||||
|
||||
answer = client.succeed("dig @192.168.1.2 two.foo")
|
||||
assert "192.168.1.4" in answer, "IP not found"
|
||||
|
||||
'';
|
||||
}
|
||||
4
clanServices/coredns/tests/vm/sops/users/admin/key.json
Normal file
4
clanServices/coredns/tests/vm/sops/users/admin/key.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"publickey": "age1qm0p4vf9jvcnn43s6l4prk8zn6cx0ep9gzvevxecv729xz540v8qa742eg",
|
||||
"type": "age"
|
||||
}
|
||||
@@ -94,6 +94,7 @@ nav:
|
||||
- reference/clanServices/index.md
|
||||
- reference/clanServices/admin.md
|
||||
- reference/clanServices/borgbackup.md
|
||||
- reference/clanServices/coredns.md
|
||||
- reference/clanServices/data-mesher.md
|
||||
- reference/clanServices/dyndns.md
|
||||
- reference/clanServices/emergency-access.md
|
||||
|
||||
Reference in New Issue
Block a user