1{ config, lib, pkgs, ... }:
2
3let
4 cfg = config.services.gns3-server;
5
6 settingsFormat = pkgs.formats.ini { };
7 configFile = settingsFormat.generate "gns3-server.conf" cfg.settings;
8
9in {
10 meta = {
11 doc = ./gns3-server.md;
12 maintainers = [ lib.maintainers.anthonyroussel ];
13 };
14
15 options = {
16 services.gns3-server = {
17 enable = lib.mkEnableOption "GNS3 Server daemon";
18
19 package = lib.mkPackageOptionMD pkgs "gns3-server" { };
20
21 auth = {
22 enable = lib.mkEnableOption "password based HTTP authentication to access the GNS3 Server";
23
24 user = lib.mkOption {
25 type = lib.types.nullOr lib.types.str;
26 default = null;
27 example = "gns3";
28 description = ''Username used to access the GNS3 Server.'';
29 };
30
31 passwordFile = lib.mkOption {
32 type = lib.types.nullOr lib.types.path;
33 default = null;
34 example = "/run/secrets/gns3-server-password";
35 description = ''
36 A file containing the password to access the GNS3 Server.
37
38 ::: {.warning}
39 This should be a string, not a nix path, since nix paths
40 are copied into the world-readable nix store.
41 :::
42 '';
43 };
44 };
45
46 settings = lib.mkOption {
47 type = lib.types.submodule { freeformType = settingsFormat.type; };
48 default = {};
49 example = { host = "127.0.0.1"; port = 3080; };
50 description = ''
51 The global options in `config` file in ini format.
52
53 Refer to <https://docs.gns3.com/docs/using-gns3/administration/gns3-server-configuration-file/>
54 for all available options.
55 '';
56 };
57
58 log = {
59 file = lib.mkOption {
60 type = lib.types.nullOr lib.types.path;
61 default = "/var/log/gns3/server.log";
62 description = ''Path of the file GNS3 Server should log to.'';
63 };
64
65 debug = lib.mkEnableOption "debug logging";
66 };
67
68 ssl = {
69 enable = lib.mkEnableOption "SSL encryption";
70
71 certFile = lib.mkOption {
72 type = lib.types.nullOr lib.types.path;
73 default = null;
74 example = "/var/lib/gns3/ssl/server.pem";
75 description = ''
76 Path to the SSL certificate file. This certificate will
77 be offered to, and may be verified by, clients.
78 '';
79 };
80
81 keyFile = lib.mkOption {
82 type = lib.types.nullOr lib.types.path;
83 default = null;
84 example = "/var/lib/gns3/ssl/server.key";
85 description = "Private key file for the certificate.";
86 };
87 };
88
89 dynamips = {
90 enable = lib.mkEnableOption ''Whether to enable Dynamips support.'';
91 package = lib.mkPackageOptionMD pkgs "dynamips" { };
92 };
93
94 ubridge = {
95 enable = lib.mkEnableOption ''Whether to enable uBridge support.'';
96 package = lib.mkPackageOptionMD pkgs "ubridge" { };
97 };
98
99 vpcs = {
100 enable = lib.mkEnableOption ''Whether to enable VPCS support.'';
101 package = lib.mkPackageOptionMD pkgs "vpcs" { };
102 };
103 };
104 };
105
106 config = let
107 flags = {
108 enableDocker = config.virtualisation.docker.enable;
109 enableLibvirtd = config.virtualisation.libvirtd.enable;
110 };
111
112 in lib.mkIf cfg.enable {
113 assertions = [
114 {
115 assertion = cfg.ssl.enable -> cfg.ssl.certFile != null;
116 message = "Please provide a certificate to use for SSL encryption.";
117 }
118 {
119 assertion = cfg.ssl.enable -> cfg.ssl.keyFile != null;
120 message = "Please provide a private key to use for SSL encryption.";
121 }
122 {
123 assertion = cfg.auth.enable -> cfg.auth.user != null;
124 message = "Please provide a username to use for HTTP authentication.";
125 }
126 {
127 assertion = cfg.auth.enable -> cfg.auth.passwordFile != null;
128 message = "Please provide a password file to use for HTTP authentication.";
129 }
130 ];
131
132 users.groups.ubridge = lib.mkIf cfg.ubridge.enable { };
133
134 security.wrappers.ubridge = lib.mkIf cfg.ubridge.enable {
135 capabilities = "cap_net_raw,cap_net_admin=eip";
136 group = "ubridge";
137 owner = "root";
138 permissions = "u=rwx,g=rx,o=r";
139 source = lib.getExe cfg.ubridge.package;
140 };
141
142 services.gns3-server.settings = lib.mkMerge [
143 {
144 Server = {
145 appliances_path = lib.mkDefault "/var/lib/gns3/appliances";
146 configs_path = lib.mkDefault "/var/lib/gns3/configs";
147 images_path = lib.mkDefault "/var/lib/gns3/images";
148 projects_path = lib.mkDefault "/var/lib/gns3/projects";
149 symbols_path = lib.mkDefault "/var/lib/gns3/symbols";
150 };
151 }
152 (lib.mkIf (cfg.ubridge.enable) {
153 Server.ubridge_path = lib.mkDefault (lib.getExe cfg.ubridge.package);
154 })
155 (lib.mkIf (cfg.auth.enable) {
156 Server = {
157 auth = lib.mkDefault (lib.boolToString cfg.auth.enable);
158 user = lib.mkDefault cfg.auth.user;
159 password = lib.mkDefault "@AUTH_PASSWORD@";
160 };
161 })
162 (lib.mkIf (cfg.vpcs.enable) {
163 VPCS.vpcs_path = lib.mkDefault (lib.getExe cfg.vpcs.package);
164 })
165 (lib.mkIf (cfg.dynamips.enable) {
166 Dynamips.dynamips_path = lib.mkDefault (lib.getExe cfg.dynamips.package);
167 })
168 ];
169
170 systemd.services.gns3-server = let
171 commandArgs = lib.cli.toGNUCommandLineShell { } {
172 config = "/etc/gns3/gns3_server.conf";
173 pid = "/run/gns3/server.pid";
174 log = cfg.log.file;
175 ssl = cfg.ssl.enable;
176 # These are implicitly not set if `null`
177 certfile = cfg.ssl.certFile;
178 certkey = cfg.ssl.keyFile;
179 };
180 in
181 {
182 description = "GNS3 Server";
183
184 after = [ "network.target" "network-online.target" ];
185 wantedBy = [ "multi-user.target" ];
186 wants = [ "network-online.target" ];
187
188 # configFile cannot be stored in RuntimeDirectory, because GNS3
189 # uses the `--config` base path to stores supplementary configuration files at runtime.
190 #
191 preStart = ''
192 install -m660 ${configFile} /etc/gns3/gns3_server.conf
193
194 ${lib.optionalString cfg.auth.enable ''
195 ${pkgs.replace-secret}/bin/replace-secret \
196 '@AUTH_PASSWORD@' \
197 "''${CREDENTIALS_DIRECTORY}/AUTH_PASSWORD" \
198 /etc/gns3/gns3_server.conf
199 ''}
200 '';
201
202 path = lib.optional flags.enableLibvirtd pkgs.qemu;
203
204 reloadTriggers = [ configFile ];
205
206 serviceConfig = {
207 ConfigurationDirectory = "gns3";
208 ConfigurationDirectoryMode = "0750";
209 DynamicUser = true;
210 Environment = "HOME=%S/gns3";
211 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
212 ExecStart = "${lib.getExe cfg.package} ${commandArgs}";
213 Group = "gns3";
214 LimitNOFILE = 16384;
215 LoadCredential = lib.mkIf cfg.auth.enable [ "AUTH_PASSWORD:${cfg.auth.passwordFile}" ];
216 LogsDirectory = "gns3";
217 LogsDirectoryMode = "0750";
218 PIDFile = "/run/gns3/server.pid";
219 Restart = "on-failure";
220 RestartSec = 5;
221 RuntimeDirectory = "gns3";
222 StateDirectory = "gns3";
223 StateDirectoryMode = "0750";
224 SupplementaryGroups = lib.optional flags.enableDocker "docker"
225 ++ lib.optional flags.enableLibvirtd "libvirtd"
226 ++ lib.optional cfg.ubridge.enable "ubridge";
227 User = "gns3";
228 WorkingDirectory = "%S/gns3";
229
230 # Hardening
231 DeviceAllow = lib.optional flags.enableLibvirtd "/dev/kvm";
232 DevicePolicy = "closed";
233 LockPersonality = true;
234 MemoryDenyWriteExecute = true;
235 NoNewPrivileges = true;
236 PrivateTmp = true;
237 PrivateUsers = true;
238 # Don't restrict ProcSubset because python3Packages.psutil requires read access to /proc/stat
239 # ProcSubset = "pid";
240 ProtectClock = true;
241 ProtectControlGroups = true;
242 ProtectHome = true;
243 ProtectHostname = true;
244 ProtectKernelLogs = true;
245 ProtectKernelModules = true;
246 ProtectKernelTunables = true;
247 ProtectProc = "invisible";
248 ProtectSystem = "strict";
249 RestrictAddressFamilies = [
250 "AF_INET"
251 "AF_INET6"
252 "AF_NETLINK"
253 "AF_UNIX"
254 "AF_PACKET"
255 ];
256 RestrictNamespaces = true;
257 RestrictRealtime = true;
258 RestrictSUIDSGID = true;
259 UMask = "0077";
260 };
261 };
262 };
263}