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