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