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