1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.spiped;
12in
13{
14 options = {
15 services.spiped = {
16 enable = mkOption {
17 type = types.bool;
18 default = false;
19 description = "Enable the spiped service module.";
20 };
21
22 config = mkOption {
23 type = types.attrsOf (
24 types.submodule ({
25 options = {
26 encrypt = mkOption {
27 type = types.bool;
28 default = false;
29 description = ''
30 Take unencrypted connections from the
31 `source` socket and send encrypted
32 connections to the `target` socket.
33 '';
34 };
35
36 decrypt = mkOption {
37 type = types.bool;
38 default = false;
39 description = ''
40 Take encrypted connections from the
41 `source` socket and send unencrypted
42 connections to the `target` socket.
43 '';
44 };
45
46 source = mkOption {
47 type = types.str;
48 description = ''
49 Address on which spiped should listen for incoming
50 connections. Must be in one of the following formats:
51 `/absolute/path/to/unix/socket`,
52 `host.name:port`,
53 `[ip.v4.ad.dr]:port` or
54 `[ipv6::addr]:port` - note that
55 hostnames are resolved when spiped is launched and are
56 not re-resolved later; thus if DNS entries change
57 spiped will continue to connect to the expired
58 address.
59 '';
60 };
61
62 target = mkOption {
63 type = types.str;
64 description = "Address to which spiped should connect.";
65 };
66
67 keyfile = mkOption {
68 type = types.path;
69 description = ''
70 Name of a file containing the spiped key.
71 As the daemon runs as the `spiped` user,
72 the key file must be readable by that user.
73 To securely manage the file within your configuration
74 consider a tool such as agenix or sops-nix.
75 '';
76 };
77
78 timeout = mkOption {
79 type = types.int;
80 default = 5;
81 description = ''
82 Timeout, in seconds, after which an attempt to connect to
83 the target or a protocol handshake will be aborted (and the
84 connection dropped) if not completed
85 '';
86 };
87
88 maxConns = mkOption {
89 type = types.int;
90 default = 100;
91 description = ''
92 Limit on the number of simultaneous connections allowed.
93 '';
94 };
95
96 waitForDNS = mkOption {
97 type = types.bool;
98 default = false;
99 description = ''
100 Wait for DNS. Normally when `spiped` is
101 launched it resolves addresses and binds to its source
102 socket before the parent process returns; with this option
103 it will daemonize first and retry failed DNS lookups until
104 they succeed. This allows `spiped` to
105 launch even if DNS isn't set up yet, but at the expense of
106 losing the guarantee that once `spiped` has
107 finished launching it will be ready to create pipes.
108 '';
109 };
110
111 disableKeepalives = mkOption {
112 type = types.bool;
113 default = false;
114 description = "Disable transport layer keep-alives.";
115 };
116
117 weakHandshake = mkOption {
118 type = types.bool;
119 default = false;
120 description = ''
121 Use fast/weak handshaking: This reduces the CPU time spent
122 in the initial connection setup, at the expense of losing
123 perfect forward secrecy.
124 '';
125 };
126
127 resolveRefresh = mkOption {
128 type = types.int;
129 default = 60;
130 description = ''
131 Resolution refresh time for the target socket, in seconds.
132 '';
133 };
134
135 disableReresolution = mkOption {
136 type = types.bool;
137 default = false;
138 description = "Disable target address re-resolution.";
139 };
140 };
141 })
142 );
143
144 default = { };
145
146 example = literalExpression ''
147 {
148 pipe1 =
149 { keyfile = "/var/lib/spiped/pipe1.key";
150 encrypt = true;
151 source = "localhost:6000";
152 target = "endpoint.example.com:7000";
153 };
154 pipe2 =
155 { keyfile = "/var/lib/spiped/pipe2.key";
156 decrypt = true;
157 source = "0.0.0.0:7000";
158 target = "localhost:3000";
159 };
160 }
161 '';
162
163 description = ''
164 Configuration for a secure pipe daemon. The daemon can be
165 started, stopped, or examined using
166 `systemctl`, under the name
167 `spiped@foo`.
168 '';
169 };
170 };
171 };
172
173 config = mkIf cfg.enable {
174 assertions = mapAttrsToList (name: c: {
175 assertion = (c.encrypt -> !c.decrypt) || (c.decrypt -> c.encrypt);
176 message = "A pipe must either encrypt or decrypt";
177 }) cfg.config;
178
179 users.groups.spiped.gid = config.ids.gids.spiped;
180 users.users.spiped = {
181 description = "Secure Pipe Service user";
182 group = "spiped";
183 uid = config.ids.uids.spiped;
184 };
185
186 systemd.services."spiped@" = {
187 description = "Secure pipe '%i'";
188 after = [ "network.target" ];
189
190 serviceConfig = {
191 Restart = "always";
192 User = "spiped";
193 };
194 stopIfChanged = false;
195
196 scriptArgs = "%i";
197 script = "exec ${pkgs.spiped}/bin/spiped -F `cat /etc/spiped/$1.spec`";
198 };
199
200 # Setup spiped config files
201 environment.etc = mapAttrs' (
202 name: cfg:
203 nameValuePair "spiped/${name}.spec" {
204 text = concatStringsSep " " [
205 (if cfg.encrypt then "-e" else "-d") # Mode
206 "-s ${cfg.source}" # Source
207 "-t ${cfg.target}" # Target
208 "-k ${cfg.keyfile}" # Keyfile
209 "-n ${toString cfg.maxConns}" # Max number of conns
210 "-o ${toString cfg.timeout}" # Timeout
211 (optionalString cfg.waitForDNS "-D") # Wait for DNS
212 (optionalString cfg.weakHandshake "-f") # No PFS
213 (optionalString cfg.disableKeepalives "-j") # Keepalives
214 (if cfg.disableReresolution then "-R" else "-r ${toString cfg.resolveRefresh}")
215 ];
216 }
217 ) cfg.config;
218 };
219}