1{
2 lib,
3 pkgs,
4 config,
5 ...
6}:
7let
8 inherit (lib)
9 mkIf
10 mkEnableOption
11 mkPackageOption
12 mkOption
13 attrNames
14 types
15 match
16 optional
17 optionals
18 toInt
19 last
20 splitString
21 allUnique
22 concatStringsSep
23 all
24 filter
25 mapAttrs
26 any
27 getExe
28 maintainers
29 ;
30 inherit (cfg) settings;
31 cfg = config.services.broadcast-box;
32
33 addressToPort = address: toInt (last (splitString ":" address));
34 httpPort = cfg.web.port;
35 tcpMuxPort = addressToPort settings.TCP_MUX_ADDRESS;
36 httpRedirect = settings.ENABLE_HTTP_REDIRECT or (settings.HTTPS_REDIRECT_PORT != null);
37
38 udpPorts =
39 optional (settings.UDP_MUX_PORT != null) settings.UDP_MUX_PORT
40 ++ optional (settings.UDP_WHEP_PORT != null) settings.UDP_WHEP_PORT
41 ++ optional (settings.UDP_WHIP_PORT != null) settings.UDP_WHIP_PORT;
42 tcpPorts = optional (settings.TCP_MUX_ADDRESS != null) tcpMuxPort;
43 webPorts = [ httpPort ] ++ optional httpRedirect settings.HTTPS_REDIRECT_PORT;
44in
45{
46 options.services.broadcast-box = {
47 enable = mkEnableOption "Broadcast Box";
48 package = mkPackageOption pkgs "broadcast-box" { };
49
50 web = {
51 host = mkOption {
52 type = types.str;
53 default = "";
54 example = "127.0.0.1";
55 description = ''
56 Host address the HTTP server listens on. By default the server
57 listens on all interfaces.
58 '';
59 };
60
61 port = mkOption {
62 type = types.port;
63 default = 8080;
64 description = ''
65 Port the HTTP server listens on.
66 '';
67 };
68
69 openFirewall = mkEnableOption ''
70 opening the HTTP server port and, if enabled, the HTTPS redirect server
71 port in the firewall.
72 '';
73 };
74
75 openFirewall = mkEnableOption ''
76 opening WebRTC traffic ports in the firewall. Randomly selected ports
77 will not be opened.
78 '';
79
80 settings = mkOption {
81 visible = "shallow";
82
83 type = types.submodule {
84 freeformType =
85 with types;
86 attrsOf (
87 nullOr (oneOf [
88 bool
89 int
90 str
91 ])
92 );
93 options = {
94 TCP_MUX_ADDRESS = mkOption {
95 type = with types; nullOr (strMatching ".*:[0-9]+");
96 default = null;
97 };
98
99 DISABLE_STATUS = mkOption {
100 type = types.bool;
101 default = true;
102 };
103
104 UDP_MUX_PORT = mkOption {
105 type = with types; nullOr port;
106 default = null;
107 };
108
109 UDP_WHEP_PORT = mkOption {
110 type = with types; nullOr port;
111 default = null;
112 };
113
114 UDP_WHIP_PORT = mkOption {
115 type = with types; nullOr port;
116 default = null;
117 };
118
119 ENABLE_HTTP_REDIRECT = mkOption {
120 type = types.bool;
121 default = false;
122 };
123
124 HTTPS_REDIRECT_PORT = mkOption {
125 type = with types; nullOr port;
126 default = if settings.ENABLE_HTTP_REDIRECT then 80 else null;
127 };
128 };
129 };
130
131 default = {
132 DISABLE_STATUS = true;
133 };
134
135 example = {
136 DISABLE_STATUS = true;
137 INCLUDE_PUBLIC_IP_IN_NAT_1_TO_1_IP = true;
138 UDP_MUX_PORT = 3000;
139 };
140
141 description = ''
142 Attribute set of environment variables.
143
144 <https://github.com/Glimesh/broadcast-box#environment-variables>
145
146 :::{.warning}
147 The status API exposes stream keys so {env}`DISABLE_STATUS` is enabled
148 by default.
149 :::
150 '';
151 };
152 };
153
154 config = mkIf cfg.enable {
155 assertions = [
156 {
157 assertion = !(settings ? HTTP_ADDRESS);
158 message = ''
159 The Broadcast Box `HTTP_ADDRESS` variable should not be used. Instead
160 use the `host` and `port` options.
161 '';
162 }
163 {
164 assertion = httpRedirect -> settings ? SSL_CERT && settings ? SSL_KEY;
165 message = ''
166 The Broadcast Box `ENABLE_HTTP_REDIRECT` variable requires `SSL_CERT`
167 and `SSL_KEY` to be configured.
168 '';
169 }
170 {
171 assertion = httpRedirect -> httpPort == 443;
172 message = ''
173 Broadcast Box HTTP redirect only works if the HTTP server listen port
174 is 443.
175 '';
176 }
177 {
178 assertion = allUnique (tcpPorts ++ webPorts);
179 message = ''
180 Broadcast Box configuration contains duplicate TCP ports.
181 '';
182 }
183 {
184 assertion = all (name: (match "[A-Z0-9_]+" name) != null) (attrNames settings);
185 message =
186 let
187 offenders = filter (name: (match "[A-Z0-9_]+" name) == null) (attrNames settings);
188 in
189 ''
190 Broadcast Box `settings` attribute names must be in uppercase snake
191 case. Invalid attribute name(s): `${concatStringsSep ", " offenders}`
192 '';
193 }
194 ];
195
196 systemd.services.broadcast-box = {
197 description = "Broadcast Box";
198 after = [ "network-online.target" ];
199 wants = [ "network-online.target" ];
200 wantedBy = [ "multi-user.target" ];
201 startLimitBurst = 3;
202 startLimitIntervalSec = 180;
203
204 environment =
205 (mapAttrs (
206 _: value:
207 if (builtins.typeOf value == "bool") then
208 if !value then null else "true"
209 else if (builtins.typeOf value == "int") then
210 toString value
211 else
212 value
213 ) cfg.settings)
214 // {
215 APP_ENV = "nixos";
216 HTTP_ADDRESS = cfg.web.host + ":" + toString cfg.web.port;
217 };
218
219 serviceConfig =
220 let
221 priviledgedPort = any (p: p > 0 && p < 1024) (udpPorts ++ tcpPorts ++ webPorts);
222 in
223 {
224 ExecStart = "${getExe cfg.package}";
225 Restart = "always";
226 RestartSec = "10s";
227
228 DynamicUser = true;
229 LockPersonality = true;
230 NoNewPrivileges = true;
231 PrivateUsers = !priviledgedPort;
232 PrivateDevices = true;
233 PrivateMounts = true;
234 PrivateTmp = true;
235 ProtectSystem = "strict";
236 ProtectHome = true;
237 ProtectControlGroups = true;
238 ProtectClock = true;
239 ProtectProc = "invisible";
240 ProtectHostname = true;
241 ProtectKernelLogs = true;
242 ProtectKernelModules = true;
243 ProtectKernelTunables = true;
244 ProcSubset = "pid";
245 RemoveIPC = true;
246 RestrictAddressFamilies = [
247 "AF_INET"
248 "AF_INET6"
249 "AF_NETLINK"
250 ];
251 RestrictNamespaces = true;
252 RestrictRealtime = true;
253 RestrictSUIDSGID = true;
254 SystemCallArchitectures = "native";
255 SystemCallFilter = [
256 "@system-service"
257 "~@privileged"
258 ];
259 CapabilityBoundingSet = if priviledgedPort then [ "CAP_NET_BIND_SERVICE" ] else "";
260 AmbientCapabilities = mkIf priviledgedPort [ "CAP_NET_BIND_SERVICE" ];
261 DeviceAllow = "";
262 MemoryDenyWriteExecute = true;
263 UMask = "0077";
264 };
265 };
266
267 networking.firewall = {
268 allowedTCPPorts = optionals cfg.openFirewall tcpPorts ++ optionals cfg.web.openFirewall webPorts;
269 allowedUDPPorts = optionals cfg.openFirewall udpPorts;
270 };
271 };
272
273 meta.maintainers = with maintainers; [ JManch ];
274}