1{ config, lib, pkgs, utils, ... }:
2with lib;
3let
4 cfg = config.services.unifi;
5 stateDir = "/var/lib/unifi";
6 cmd = "@${pkgs.jre}/bin/java java -jar ${stateDir}/lib/ace.jar";
7 mountPoints = [
8 {
9 what = "${pkgs.unifi}/dl";
10 where = "${stateDir}/dl";
11 }
12 {
13 what = "${pkgs.unifi}/lib";
14 where = "${stateDir}/lib";
15 }
16 {
17 what = "${pkgs.mongodb}/bin";
18 where = "${stateDir}/bin";
19 }
20 {
21 what = "${cfg.dataDir}";
22 where = "${stateDir}/data";
23 }
24 ];
25 systemdMountPoints = map (m: "${utils.escapeSystemdPath m.where}.mount") mountPoints;
26in
27{
28
29 options = {
30
31 services.unifi.enable = mkOption {
32 type = types.bool;
33 default = false;
34 description = ''
35 Whether or not to enable the unifi controller service.
36 '';
37 };
38
39 services.unifi.dataDir = mkOption {
40 type = types.str;
41 default = "${stateDir}/data";
42 description = ''
43 Where to store the database and other data.
44
45 This directory will be bind-mounted to ${stateDir}/data as part of the service startup.
46 '';
47 };
48
49 services.unifi.openPorts = mkOption {
50 type = types.bool;
51 default = true;
52 description = ''
53 Whether or not to open the minimum required ports on the firewall.
54
55 This is necessary to allow firmware upgrades and device discovery to
56 work. For remote login, you should additionally open (or forward) port
57 8443.
58 '';
59 };
60
61 };
62
63 config = mkIf cfg.enable {
64
65 users.extraUsers.unifi = {
66 uid = config.ids.uids.unifi;
67 description = "UniFi controller daemon user";
68 home = "${stateDir}";
69 };
70
71 networking.firewall = mkIf cfg.openPorts {
72 # https://help.ubnt.com/hc/en-us/articles/204910084-UniFi-Change-Default-Ports-for-Controller-and-UAPs
73 allowedTCPPorts = [
74 8080 # Port for UAP to inform controller.
75 8880 # Port for HTTP portal redirect, if guest portal is enabled.
76 8843 # Port for HTTPS portal redirect, ditto.
77 ];
78 allowedUDPPorts = [
79 3478 # UDP port used for STUN.
80 10001 # UDP port used for device discovery.
81 ];
82 };
83
84 # We must create the binary directories as bind mounts instead of symlinks
85 # This is because the controller resolves all symlinks to absolute paths
86 # to be used as the working directory.
87 systemd.mounts = map ({ what, where }: {
88 bindsTo = [ "unifi.service" ];
89 partOf = [ "unifi.service" ];
90 unitConfig.RequiresMountsFor = stateDir;
91 options = "bind";
92 what = what;
93 where = where;
94 }) mountPoints;
95
96 systemd.services.unifi = {
97 description = "UniFi controller daemon";
98 wantedBy = [ "multi-user.target" ];
99 after = [ "network.target" ] ++ systemdMountPoints;
100 partOf = systemdMountPoints;
101 bindsTo = systemdMountPoints;
102 unitConfig.RequiresMountsFor = stateDir;
103 # This a HACK to fix missing dependencies of dynamic libs extracted from jars
104 environment.LD_LIBRARY_PATH = with pkgs.stdenv; "${cc.cc.lib}/lib";
105
106 preStart = ''
107 # Ensure privacy of state and data.
108 chown unifi "${stateDir}" "${stateDir}/data"
109 chmod 0700 "${stateDir}" "${stateDir}/data"
110
111 # Create the volatile webapps
112 rm -rf "${stateDir}/webapps"
113 mkdir -p "${stateDir}/webapps"
114 chown unifi "${stateDir}/webapps"
115 ln -s "${pkgs.unifi}/webapps/ROOT" "${stateDir}/webapps/ROOT"
116 '';
117
118 postStop = ''
119 rm -rf "${stateDir}/webapps"
120 '';
121
122 serviceConfig = {
123 Type = "simple";
124 ExecStart = "${cmd} start";
125 ExecStop = "${cmd} stop";
126 User = "unifi";
127 PermissionsStartOnly = true;
128 UMask = "0077";
129 WorkingDirectory = "${stateDir}";
130 };
131 };
132
133 };
134
135}