1{ config, options, lib, pkgs, utils, ... }:
2let
3 cfg = config.services.unifi;
4 stateDir = "/var/lib/unifi";
5 cmd = lib.escapeShellArgs ([ "@${cfg.jrePackage}/bin/java" "java" ]
6 ++ lib.optionals (lib.versionAtLeast (lib.getVersion cfg.jrePackage) "16") [
7 "--add-opens=java.base/java.lang=ALL-UNNAMED"
8 "--add-opens=java.base/java.time=ALL-UNNAMED"
9 "--add-opens=java.base/sun.security.util=ALL-UNNAMED"
10 "--add-opens=java.base/java.io=ALL-UNNAMED"
11 "--add-opens=java.rmi/sun.rmi.transport=ALL-UNNAMED"
12 ]
13 ++ (lib.optional (cfg.initialJavaHeapSize != null) "-Xms${(toString cfg.initialJavaHeapSize)}m")
14 ++ (lib.optional (cfg.maximumJavaHeapSize != null) "-Xmx${(toString cfg.maximumJavaHeapSize)}m")
15 ++ cfg.extraJvmOptions
16 ++ [ "-jar" "${stateDir}/lib/ace.jar" ]);
17in
18{
19
20 options = {
21
22 services.unifi.enable = lib.mkOption {
23 type = lib.types.bool;
24 default = false;
25 description = ''
26 Whether or not to enable the unifi controller service.
27 '';
28 };
29
30 services.unifi.jrePackage = lib.mkOption {
31 type = lib.types.package;
32 default = if (lib.versionAtLeast (lib.getVersion cfg.unifiPackage) "7.5") then pkgs.jdk17_headless else if (lib.versionAtLeast (lib.getVersion cfg.unifiPackage) "7.3") then pkgs.jdk11 else pkgs.jre8;
33 defaultText = lib.literalExpression ''if (lib.versionAtLeast (lib.getVersion cfg.unifiPackage) "7.5") then pkgs.jdk17_headless else if (lib.versionAtLeast (lib.getVersion cfg.unifiPackage) "7.3" then pkgs.jdk11 else pkgs.jre8'';
34 description = ''
35 The JRE package to use. Check the release notes to ensure it is supported.
36 '';
37 };
38
39 services.unifi.unifiPackage = lib.mkPackageOption pkgs "unifi5" { };
40
41 services.unifi.mongodbPackage = lib.mkPackageOption pkgs "mongodb" {
42 default = "mongodb-5_0";
43 extraDescription = ''
44 ::: {.note}
45 unifi7 officially only supports mongodb up until 4.4 but works with 5.0.
46 :::
47 '';
48 };
49
50 services.unifi.openFirewall = lib.mkOption {
51 type = lib.types.bool;
52 default = false;
53 description = ''
54 Whether or not to open the minimum required ports on the firewall.
55
56 This is necessary to allow firmware upgrades and device discovery to
57 work. For remote login, you should additionally open (or forward) port
58 8443.
59 '';
60 };
61
62 services.unifi.initialJavaHeapSize = lib.mkOption {
63 type = with lib.types; nullOr int;
64 default = null;
65 example = 1024;
66 description = ''
67 Set the initial heap size for the JVM in MB. If this option isn't set, the
68 JVM will decide this value at runtime.
69 '';
70 };
71
72 services.unifi.maximumJavaHeapSize = lib.mkOption {
73 type = with lib.types; nullOr int;
74 default = null;
75 example = 4096;
76 description = ''
77 Set the maximum heap size for the JVM in MB. If this option isn't set, the
78 JVM will decide this value at runtime.
79 '';
80 };
81
82 services.unifi.extraJvmOptions = lib.mkOption {
83 type = with lib.types; listOf str;
84 default = [ ];
85 example = lib.literalExpression ''["-Xlog:gc"]'';
86 description = ''
87 Set extra options to pass to the JVM.
88 '';
89 };
90
91 };
92
93 config = lib.mkIf cfg.enable {
94
95 users.users.unifi = {
96 isSystemUser = true;
97 group = "unifi";
98 description = "UniFi controller daemon user";
99 home = "${stateDir}";
100 };
101 users.groups.unifi = {};
102
103 networking.firewall = lib.mkIf cfg.openFirewall {
104 # https://help.ubnt.com/hc/en-us/articles/218506997
105 allowedTCPPorts = [
106 8080 # Port for UAP to inform controller.
107 8880 # Port for HTTP portal redirect, if guest portal is enabled.
108 8843 # Port for HTTPS portal redirect, ditto.
109 6789 # Port for UniFi mobile speed test.
110 ];
111 allowedUDPPorts = [
112 3478 # UDP port used for STUN.
113 10001 # UDP port used for device discovery.
114 ];
115 };
116
117 systemd.services.unifi = {
118 description = "UniFi controller daemon";
119 wantedBy = [ "multi-user.target" ];
120 after = [ "network.target" ];
121
122 # This a HACK to fix missing dependencies of dynamic libs extracted from jars
123 environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
124 # Make sure package upgrades trigger a service restart
125 restartTriggers = [ cfg.unifiPackage cfg.mongodbPackage ];
126
127 serviceConfig = {
128 Type = "simple";
129 ExecStart = "${cmd} start";
130 ExecStop = "${cmd} stop";
131 Restart = "on-failure";
132 TimeoutSec = "5min";
133 User = "unifi";
134 UMask = "0077";
135 WorkingDirectory = "${stateDir}";
136 # the stop command exits while the main process is still running, and unifi
137 # wants to manage its own child processes. this means we have to set KillSignal
138 # to something the main process ignores, otherwise every stop will have unifi.service
139 # fail with SIGTERM status.
140 KillSignal = "SIGCONT";
141
142 # Hardening
143 AmbientCapabilities = "";
144 CapabilityBoundingSet = "";
145 # ProtectClock= adds DeviceAllow=char-rtc r
146 DeviceAllow = "";
147 DevicePolicy = "closed";
148 LockPersonality = true;
149 NoNewPrivileges = true;
150 PrivateDevices = true;
151 PrivateMounts = true;
152 PrivateTmp = true;
153 PrivateUsers = true;
154 ProtectClock = true;
155 ProtectControlGroups = true;
156 ProtectHome = true;
157 ProtectHostname = true;
158 ProtectKernelLogs = true;
159 ProtectKernelModules = true;
160 ProtectKernelTunables = true;
161 ProtectSystem = "strict";
162 RemoveIPC = true;
163 RestrictNamespaces = true;
164 RestrictRealtime = true;
165 RestrictSUIDSGID = true;
166 SystemCallErrorNumber = "EPERM";
167 SystemCallFilter = [ "@system-service" ];
168
169 StateDirectory = "unifi";
170 RuntimeDirectory = "unifi";
171 LogsDirectory = "unifi";
172 CacheDirectory = "unifi";
173
174 TemporaryFileSystem = [
175 # required as we want to create bind mounts below
176 "${stateDir}/webapps:rw"
177 ];
178
179 # We must create the binary directories as bind mounts instead of symlinks
180 # This is because the controller resolves all symlinks to absolute paths
181 # to be used as the working directory.
182 BindPaths = [
183 "/var/log/unifi:${stateDir}/logs"
184 "/run/unifi:${stateDir}/run"
185 "${cfg.unifiPackage}/dl:${stateDir}/dl"
186 "${cfg.unifiPackage}/lib:${stateDir}/lib"
187 "${cfg.mongodbPackage}/bin:${stateDir}/bin"
188 "${cfg.unifiPackage}/webapps/ROOT:${stateDir}/webapps/ROOT"
189 ];
190
191 # Needs network access
192 PrivateNetwork = false;
193 # Cannot be true due to OpenJDK
194 MemoryDenyWriteExecute = false;
195 };
196 };
197
198 };
199 imports = [
200 (lib.mkRemovedOptionModule [ "services" "unifi" "dataDir" ] "You should move contents of dataDir to /var/lib/unifi/data")
201 (lib.mkRenamedOptionModule [ "services" "unifi" "openPorts" ] [ "services" "unifi" "openFirewall" ])
202 ];
203}