1 { pkgs, lib, config, ... }:
4 inherit (config.users) users groups;
5 cfg = config.services.upnpc;
7 while IFS=: read -r k v; do
8 k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//')
9 v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//')
12 ("Local LAN ip address") localIP=$v;;
20 options.services.upnpc = {
21 redirections = mkOption {
22 description = "UPnP redirections to request.";
24 type = types.listOf (types.submodule ({config, ...}: {
25 options.externalPort = mkOption {
26 description = "External port to open on the redirecting device.";
29 options.internalPort = mkOption {
30 description = "Internal port, target of the redirection.";
32 default = config.externalPort;
34 options.protocol = mkOption {
35 description = "Protocol to redirect.";
36 type = with types; enum ["TCP" "UDP"];
39 options.description = mkOption {
40 description = "Description of the port mapping";
44 options.duration = mkOption {
45 description = "Duration of the redirection, in seconds. 0 means indefinitely.";
49 options.maintainPeriod = mkOption {
50 description = "Period (in seconds) between runs to maintain the redirection.";
51 type = with types; nullOr int;
52 default = if config.duration > 0 then config.duration / 2 else null;
53 defaultText = "if duration > 0 then duration / 2 else null";
55 options.override = mkOption {
56 description = "Try to override the redirection in case of conflict in mapping entry.";
60 options.service = mkOption {
61 description = "Configuration specific to the systemd service handling this UPnP redirecting.";
69 systemd.services = listToAttrs (map (r:
70 nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [
71 { description = "UPnP ${toString r.internalPort}";
72 after = [ "network-pre.target" ];
73 #wantedBy = [ "multi-user.target" ];
74 path = [ pkgs.miniupnpc ];
76 Type = if r.maintainPeriod == null then "oneshot" else "simple";
77 RemainAfterExit = r.maintainPeriod == null;
78 ExecStart = pkgs.writeShellScript "upnpc-start-${toString r.internalPort}" ''
82 while IFS= read -r line; do
85 (*" is redirected to internal $localIP:${toString r.internalPort}"*) result=ok ;;
86 (*ConflictInMappingEntry*) result=conflict ;;
89 $(upnpc -u "$desc" ${optionalString (r.description != "") "-e \"${r.description}\""} \
90 -a "$localIP" ${toString r.internalPort} ${toString r.externalPort} ${r.protocol} ${toString r.duration} 2>&1)
96 ${optionalString r.override ''
97 test "$result" != conflict || {
98 upnpc -u "$desc" -d ${toString r.externalPort} ${r.protocol}
103 (ok) ${if r.maintainPeriod == null then "break" else "sleep " + toString r.maintainPeriod} ;;
108 ExecStop = "${pkgs.miniupnpc}/bin/upnpc -d ${toString r.externalPort} ${r.protocol}";
109 Restart = "on-failure";
110 RestartSec = mkDefault r.maintainPeriod;
112 User = users."upnpc".name;
119 # This enables to match on the uid in the firewall.
120 users.users."upnpc" = {
122 group = groups."upnpc".name;
124 users.groups."upnpc" = {};
126 meta.maintainers = with maintainers; [ julm ];