Skip to content

Commit 9825c04

Browse files
authored
Merge pull request #175 from keatonhasse/add-default-url
Add default url option and allow specifying only the port in services
2 parents 993fee0 + 56a2d92 commit 9825c04

File tree

4 files changed

+340
-4
lines changed

4 files changed

+340
-4
lines changed

nixos/default.nix

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,49 @@
44
lib,
55
...
66
}: let
7-
serviceSubmodule = with lib; let
7+
urlPartsSubmodule.options = with lib; {
8+
protocol = mkOption {
9+
description = "URL Scheme or protocol to use for reaching the upstream service.";
10+
type = types.str;
11+
default = "http";
12+
};
13+
14+
host = mkOption {
15+
description = "Host where the upstream service can be reached.";
16+
type = types.str;
17+
};
18+
19+
port = mkOption {
20+
description = "Port where the upstream service can be reached.";
21+
type = types.int;
22+
};
23+
};
24+
25+
urlPartsSubmoduleWithDefaults.options = with lib; let
826
inherit (config.services.tsnsrv) defaults;
927
in {
28+
protocol = mkOption {
29+
description = "URL Scheme or protocol to use for reaching the upstream service.";
30+
type = types.str;
31+
default = defaults.urlParts.protocol;
32+
};
33+
34+
host = mkOption {
35+
description = "Host where the upstream service can be reached.";
36+
type = types.str;
37+
default = defaults.urlParts.host;
38+
};
39+
40+
port = mkOption {
41+
description = "Port where the upstream service can be reached.";
42+
type = types.port;
43+
default = defaults.urlParts.port;
44+
};
45+
};
46+
47+
serviceSubmodule = with lib; let
48+
inherit (config.services.tsnsrv) defaults;
49+
in ({config, ...}: {
1050
options = {
1151
authKeyPath = mkOption {
1252
description = "Path to a file containing a tailscale auth key. Make this a secret";
@@ -50,7 +90,7 @@
5090

5191
package = mkOption {
5292
description = "Package to use for this tsnsrv service.";
53-
default = config.services.tsnsrv.defaults.package;
93+
default = defaults.package;
5494
type = types.package;
5595
};
5696

@@ -129,9 +169,16 @@
129169
default = null;
130170
};
131171

172+
urlParts = mkOption {
173+
description = "URL parts that make up an alternative to the toURL option.";
174+
type = types.nullOr (types.submodule urlPartsSubmoduleWithDefaults);
175+
default = null;
176+
};
177+
132178
toURL = mkOption {
133-
description = "URL to forward HTTP requests to";
179+
description = "URL to forward HTTP requests to. Either this or the urlParts option must be set.";
134180
type = types.str;
181+
default = "${config.urlParts.protocol}://${config.urlParts.host}:${builtins.toString config.urlParts.port}";
135182
};
136183

137184
supplementalGroups = mkOption {
@@ -175,7 +222,7 @@
175222
default = [];
176223
};
177224
};
178-
};
225+
});
179226

180227
serviceArgs = {
181228
name,
@@ -300,6 +347,11 @@ in {
300347
type = types.bool;
301348
default = false;
302349
};
350+
351+
urlParts = mkOption {
352+
description = "Default URL parts for tsnsrv services. Each service will have the parts here interpolated onto its .toURL option by default.";
353+
type = types.submodule urlPartsSubmodule;
354+
};
303355
};
304356

305357
services.tsnsrv.services = mkOption {

nixos/tests/e2e/default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,7 @@
55
...
66
}: {
77
systemd = import ./systemd.nix {inherit pkgs nixos-lib nixosModule;};
8+
systemd-urlparts = import ./systemd-urlparts.nix {inherit pkgs nixos-lib nixosModule;};
89
oci = import ./oci.nix {inherit pkgs nixos-lib nixosModule;};
10+
oci-urlparts = import ./oci-urlparts.nix {inherit pkgs nixos-lib nixosModule;};
911
}

nixos/tests/e2e/oci-urlparts.nix

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
{
2+
pkgs,
3+
nixos-lib,
4+
nixosModule,
5+
}: let
6+
stunPort = 3478;
7+
in
8+
nixos-lib.runTest {
9+
name = "tsnsrv-nixos";
10+
hostPkgs = pkgs;
11+
12+
nodes.headscale = {
13+
environment.systemPackages = [pkgs.headscale];
14+
services.headscale = {
15+
enable = true;
16+
address = "[::]";
17+
settings = {
18+
ip_prefixes = ["100.64.0.0/10"];
19+
dns.magic_dns = false;
20+
derp.server = {
21+
enabled = true;
22+
region_id = 999;
23+
stun_listen_addr = "0.0.0.0:${toString stunPort}";
24+
};
25+
server_url = "http://headscale:8080";
26+
};
27+
};
28+
networking.firewall = {
29+
allowedTCPPorts = [8080 443];
30+
allowedUDPPorts = [stunPort];
31+
};
32+
};
33+
34+
nodes.machine = {
35+
config,
36+
pkgs,
37+
lib,
38+
...
39+
}: {
40+
imports = [
41+
nixosModule
42+
];
43+
44+
environment.systemPackages = [pkgs.tailscale];
45+
virtualisation.cores = 4;
46+
virtualisation.memorySize = 1024;
47+
services.tailscale.enable = true;
48+
systemd.services.tailscaled.serviceConfig.Environment = ["TS_NO_LOGS_NO_SUPPORT=true"];
49+
50+
services.tsnsrv = {
51+
enable = true;
52+
defaults.tsnetVerbose = true;
53+
defaults.loginServerUrl = "http://headscale:8080";
54+
defaults.authKeyPath = "/run/ts-authkey";
55+
defaults.urlParts.host = "127.0.0.1";
56+
};
57+
virtualisation.oci-sidecars.tsnsrv = {
58+
enable = true;
59+
containers.web-server-tsnsrv = {
60+
name = "web-server";
61+
forContainer = "web-server";
62+
service = {
63+
timeout = "10s";
64+
listenAddr = ":80";
65+
plaintext = true;
66+
urlParts.port = 3000;
67+
};
68+
};
69+
};
70+
virtualisation.oci-containers = let
71+
htmlRoot = pkgs.writeTextDir "index.html" "It works!";
72+
in {
73+
backend = "podman";
74+
containers.web-server = {
75+
image = "web-server:latest";
76+
imageFile = pkgs.dockerTools.buildImage {
77+
name = "web-server";
78+
tag = "latest";
79+
created = "now";
80+
copyToRoot = pkgs.buildEnv {
81+
name = "image-root";
82+
paths = [pkgs.static-web-server htmlRoot];
83+
pathsToLink = ["/bin"];
84+
};
85+
config.Cmd = ["/bin/static-web-server" "--port" "3000" "--root" htmlRoot];
86+
};
87+
};
88+
};
89+
systemd.services.podman-web-server-tsnsrv.enableStrictShellChecks = true;
90+
networking.firewall.trustedInterfaces = ["podman0"];
91+
92+
# Delay starting the container machinery until we have an authkey:
93+
systemd.services.podman-web-server.serviceConfig.ConditionPathExists = "/run/ts-authkey";
94+
95+
# Serve DNS to the podman containers, otherwise they have no idea who headscale is:
96+
virtualisation.podman.defaultNetwork.settings.dns_enabled = true;
97+
services.resolved = {
98+
enable = true;
99+
};
100+
};
101+
102+
testScript = ''
103+
import time
104+
import json
105+
106+
headscale.start()
107+
machine.start()
108+
109+
headscale.wait_for_unit("headscale.service", timeout=30)
110+
headscale.succeed("headscale users create machine")
111+
authkey = headscale.succeed("headscale preauthkeys create --reusable -e 24h -u machine")
112+
with open("authkey", "w") as k:
113+
k.write(authkey)
114+
115+
machine.copy_from_host("authkey", "/run/ts-authkey")
116+
machine.wait_for_unit("tailscaled.service", timeout=30)
117+
machine.succeed('tailscale up --login-server=http://headscale:8080 --auth-key="$(cat /run/ts-authkey)"')
118+
119+
@polling_condition
120+
def tsnsrv_running():
121+
machine.succeed("systemctl is-active podman-web-server-tsnsrv")
122+
123+
def wait_for_tsnsrv_registered():
124+
"Poll until tsnsrv appears in the list of hosts, then return its IP."
125+
while True:
126+
output = json.loads(headscale.succeed("headscale nodes list -o json-line"))
127+
basic_entry = [elt["ip_addresses"][0] for elt in output if elt["given_name"] == "web-server"]
128+
if len(basic_entry) == 1:
129+
return basic_entry[0]
130+
time.sleep(1)
131+
132+
def test_script_e2e():
133+
headscale.wait_until_succeeds("headscale nodes list -o json-line")
134+
machine.wait_for_unit("podman-web-server-tsnsrv", timeout=30)
135+
with tsnsrv_running:
136+
# We don't have magic DNS in this setup, so let's figure out the IP from the node list:
137+
tsnsrv_ip = wait_for_tsnsrv_registered()
138+
print(f"tsnsrv seems up, with IP {tsnsrv_ip}")
139+
machine.wait_until_succeeds(f"tailscale ping {tsnsrv_ip}", timeout=30)
140+
print(machine.succeed(f"curl -f http://{tsnsrv_ip}"))
141+
test_script_e2e()
142+
143+
'';
144+
}

nixos/tests/e2e/systemd-urlparts.nix

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
{
2+
pkgs,
3+
nixos-lib,
4+
nixosModule,
5+
}: let
6+
stunPort = 3478;
7+
in
8+
nixos-lib.runTest {
9+
name = "tsnsrv-nixos";
10+
hostPkgs = pkgs;
11+
12+
defaults.services.tsnsrv.enable = true;
13+
defaults.services.tsnsrv.defaults.tsnetVerbose = true;
14+
15+
nodes.machine = {
16+
config,
17+
pkgs,
18+
lib,
19+
...
20+
}: {
21+
imports = [
22+
nixosModule
23+
];
24+
25+
environment.systemPackages = [
26+
pkgs.headscale
27+
pkgs.tailscale
28+
(pkgs.writeShellApplication {
29+
name = "tailscale-up-for-tests";
30+
text = ''
31+
systemctl start --wait generate-tsnsrv-authkey@tailscaled.service
32+
tailscale up \
33+
--login-server=${config.services.headscale.settings.server_url} \
34+
--auth-key="$(cat /var/lib/headscale-authkeys/tailscaled.preauth-key)"
35+
'';
36+
})
37+
];
38+
virtualisation.cores = 4;
39+
virtualisation.memorySize = 1024;
40+
services.headscale = {
41+
enable = true;
42+
settings = {
43+
ip_prefixes = ["100.64.0.0/10"];
44+
dns.magic_dns = false;
45+
derp.server = {
46+
enabled = true;
47+
region_id = 999;
48+
stun_listen_addr = "0.0.0.0:${toString stunPort}";
49+
};
50+
};
51+
};
52+
services.tailscale.enable = true;
53+
systemd.services.tailscaled.serviceConfig.Environment = ["TS_NO_LOGS_NO_SUPPORT=true"];
54+
networking.firewall = {
55+
allowedTCPPorts = [80 443];
56+
allowedUDPPorts = [stunPort];
57+
};
58+
59+
systemd.services."generate-tsnsrv-authkey@" = {
60+
description = "Generate headscale authkey for %i";
61+
serviceConfig.ExecStart = let
62+
startScript = pkgs.writeShellApplication {
63+
name = "generate-tsnsrv-authkey";
64+
runtimeInputs = [pkgs.headscale pkgs.jq];
65+
text = ''
66+
set -x
67+
headscale users create "$1"
68+
headscale preauthkeys create --reusable -e 24h -u "$1" > "$STATE_DIRECTORY"/"$1".preauth-key
69+
echo generated "$STATE_DIRECTORY"/"$1".preauth-key
70+
cat "$STATE_DIRECTORY"/"$1".preauth-key
71+
'';
72+
};
73+
in "${lib.getExe startScript} %i";
74+
wants = ["headscale.service"];
75+
after = ["headscale.service"];
76+
serviceConfig.Type = "oneshot";
77+
serviceConfig.StateDirectory = "headscale-authkeys";
78+
serviceConfig.Group = "tsnsrv";
79+
unitConfig.Requires = ["headscale.service"];
80+
};
81+
82+
systemd.services.tsnsrv-basic = {
83+
enableStrictShellChecks = true;
84+
wants = ["generate-tsnsrv-authkey@basic.service"];
85+
after = ["generate-tsnsrv-authkey@basic.service"];
86+
unitConfig.Requires = ["generate-tsnsrv-authkey@basic.service"];
87+
};
88+
services.static-web-server = {
89+
enable = true;
90+
listen = "127.0.0.1:3000";
91+
root = pkgs.writeTextDir "index.html" "It works!";
92+
};
93+
services.tsnsrv = {
94+
defaults.urlParts.host = "127.0.0.1";
95+
defaults.loginServerUrl = config.services.headscale.settings.server_url;
96+
defaults.authKeyPath = "/var/lib/headscale-authkeys/basic.preauth-key";
97+
services.basic = {
98+
timeout = "10s";
99+
listenAddr = ":80";
100+
plaintext = true; # HTTPS requires certs
101+
urlParts.port = 3000;
102+
};
103+
};
104+
};
105+
106+
testScript = ''
107+
machine.start()
108+
machine.wait_for_unit("tailscaled.service", timeout=30)
109+
machine.succeed("tailscale-up-for-tests", timeout=30)
110+
import time
111+
import json
112+
113+
@polling_condition
114+
def tsnsrv_running():
115+
machine.succeed("systemctl is-active tsnsrv-basic")
116+
117+
def wait_for_tsnsrv_registered():
118+
"Poll until tsnsrv appears in the list of hosts, then return its IP."
119+
while True:
120+
output = json.loads(machine.succeed("headscale nodes list -o json-line"))
121+
basic_entry = [elt["ip_addresses"][0] for elt in output if elt["given_name"] == "basic"]
122+
if len(basic_entry) == 1:
123+
return basic_entry[0]
124+
time.sleep(1)
125+
126+
def test_script_e2e():
127+
machine.wait_until_succeeds("headscale nodes list -o json-line")
128+
machine.wait_for_unit("tsnsrv-basic", timeout=30)
129+
with tsnsrv_running:
130+
# We don't have magic DNS in this setup, so let's figure out the IP from the node list:
131+
tsnsrv_ip = wait_for_tsnsrv_registered()
132+
print(f"tsnsrv seems up, with IP {tsnsrv_ip}")
133+
machine.wait_until_succeeds(f"tailscale ping {tsnsrv_ip}", timeout=30)
134+
print(machine.succeed(f"curl -f http://{tsnsrv_ip}"))
135+
test_script_e2e()
136+
137+
'';
138+
}

0 commit comments

Comments
 (0)