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