1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.miniupnpd;
7 configFile = pkgs.writeText "miniupnpd.conf" ''
8 ext_ifname=${cfg.externalInterface}
9 enable_natpmp=${if cfg.natpmp then "yes" else "no"}
10 enable_upnp=${if cfg.upnp then "yes" else "no"}
11
12 ${concatMapStrings (range: ''
13 listening_ip=${range}
14 '') cfg.internalIPs}
15
16 ${lib.optionalString (firewall == "nftables") ''
17 upnp_table_name=miniupnpd
18 upnp_nat_table_name=miniupnpd
19 ''}
20
21 ${cfg.appendConfig}
22 '';
23 firewall = if config.networking.nftables.enable then "nftables" else "iptables";
24 miniupnpd = pkgs.miniupnpd.override { inherit firewall; };
25 firewallScripts = lib.optionals (firewall == "iptables")
26 ([ "iptables"] ++ lib.optional (config.networking.enableIPv6) "ip6tables");
27in
28{
29 options = {
30 services.miniupnpd = {
31 enable = mkEnableOption "MiniUPnP daemon";
32
33 externalInterface = mkOption {
34 type = types.str;
35 description = ''
36 Name of the external interface.
37 '';
38 };
39
40 internalIPs = mkOption {
41 type = types.listOf types.str;
42 example = [ "192.168.1.1/24" "enp1s0" ];
43 description = ''
44 The IP address ranges to listen on.
45 '';
46 };
47
48 natpmp = mkEnableOption "NAT-PMP support";
49
50 upnp = mkOption {
51 default = true;
52 type = types.bool;
53 description = ''
54 Whether to enable UPNP support.
55 '';
56 };
57
58 appendConfig = mkOption {
59 type = types.lines;
60 default = "";
61 description = ''
62 Configuration lines appended to the MiniUPnP config.
63 '';
64 };
65 };
66 };
67
68 config = mkIf cfg.enable {
69 networking.firewall.extraCommands = lib.mkIf (firewallScripts != []) (builtins.concatStringsSep "\n" (map (fw: ''
70 EXTIF=${cfg.externalInterface} ${pkgs.bash}/bin/bash -x ${miniupnpd}/etc/miniupnpd/${fw}_init.sh
71 '') firewallScripts));
72
73 networking.firewall.extraStopCommands = lib.mkIf (firewallScripts != []) (builtins.concatStringsSep "\n" (map (fw: ''
74 EXTIF=${cfg.externalInterface} ${pkgs.bash}/bin/bash -x ${miniupnpd}/etc/miniupnpd/${fw}_removeall.sh
75 '') firewallScripts));
76
77 networking.nftables = lib.mkIf (firewall == "nftables") {
78 # see nft_init in ${miniupnpd-nftables}/etc/miniupnpd
79 tables.miniupnpd = {
80 family = "inet";
81 # The following is omitted because it's expected that the firewall is to be responsible for it.
82 #
83 # chain forward {
84 # type filter hook forward priority filter; policy drop;
85 # jump miniupnpd
86 # }
87 #
88 # Otherwise, it quickly gets ugly with (potentially) two forward chains with "policy drop".
89 # This means the chain "miniupnpd" never actually gets triggered and is simply there to satisfy
90 # miniupnpd. If you're doing it yourself (without networking.firewall), the easiest way to get
91 # it to work is adding a rule "ct status dnat accept" - this is what networking.firewall does.
92 # If you don't want to simply accept forwarding for all "ct status dnat" packets, override
93 # upnp_table_name with whatever your table is, create a chain "miniupnpd" in your table and
94 # jump into it from your forward chain.
95 content = ''
96 chain miniupnpd {}
97 chain prerouting_miniupnpd {
98 type nat hook prerouting priority dstnat; policy accept;
99 }
100 chain postrouting_miniupnpd {
101 type nat hook postrouting priority srcnat; policy accept;
102 }
103 '';
104 };
105 };
106
107 systemd.services.miniupnpd = {
108 description = "MiniUPnP daemon";
109 after = [ "network.target" ];
110 wantedBy = [ "multi-user.target" ];
111 serviceConfig = {
112 ExecStart = "${miniupnpd}/bin/miniupnpd -f ${configFile}";
113 PIDFile = "/run/miniupnpd.pid";
114 Type = "forking";
115 };
116 };
117 };
118}