{ pkgs, lib, config, ... }: with lib; let inherit (config.users) users groups; cfg = config.services.upnpc; getInfo = '' while IFS=: read -r k v; do k=$(printf %s "$k" | sed -e 's/^\s*//' -e 's/\s*$//') v=$(printf %s "$v" | sed -e 's/^\s*//' -e 's/\s*$//') case $k in (desc) desc=$v;; ("Local LAN ip address") localIP=$v;; esac done <<EOF $(upnpc -s) EOF ''; in { options.services.upnpc = { redirections = mkOption { description = "UPnP redirections to request."; default = []; type = types.listOf (types.submodule ({config, ...}: { options.externalPort = mkOption { description = "External port to open on the redirecting device."; type = types.port; }; options.internalPort = mkOption { description = "Internal port, target of the redirection."; type = types.port; default = config.externalPort; }; options.protocol = mkOption { description = "Protocol to redirect."; type = with types; enum ["TCP" "UDP"]; default = "TCP"; }; options.description = mkOption { description = "Description of the port mapping"; type = types.str; default = ""; }; options.duration = mkOption { description = "Duration of the redirection, in seconds. 0 means indefinitely."; type = types.int; default = 0; }; options.maintainPeriod = mkOption { description = "Period (in seconds) between runs to maintain the redirection."; type = with types; nullOr int; default = if config.duration > 0 then config.duration / 2 else null; defaultText = "if duration > 0 then duration / 2 else null"; }; options.override = mkOption { description = "Try to override the redirection in case of conflict in mapping entry."; type = types.bool; default = true; }; options.service = mkOption { description = "Configuration specific to the systemd service handling this UPnP redirecting."; type = types.attrs; default = {}; }; })); }; }; config = { systemd.services = listToAttrs (map (r: nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [ { description = "UPnP ${toString r.internalPort}"; after = [ "network-online.target" ]; #wantedBy = [ "multi-user.target" ]; path = [ pkgs.miniupnpc ]; serviceConfig = { Type = if r.maintainPeriod == null then "oneshot" else "simple"; RemainAfterExit = r.maintainPeriod == null; ExecStart = pkgs.writeShellScript "upnpc-start-${toString r.internalPort}" '' set -eu redirect () { result= while IFS= read -r line; do echo >&2 -E "$line" case $line in (*" is redirected to internal $localIP:${toString r.internalPort}"*) result=ok ;; (*ConflictInMappingEntry*) result=conflict ;; esac done <<EOF $(upnpc -u "$desc" ${optionalString (r.description != "") "-e \"${r.description}\""} \ -a "$localIP" ${toString r.internalPort} ${toString r.externalPort} ${r.protocol} ${toString r.duration} 2>&1) EOF } while true; do ${getInfo} redirect ${optionalString r.override '' test "$result" != conflict || { upnpc -u "$desc" -d ${toString r.externalPort} ${r.protocol} redirect } ''} case $result in (ok) ${if r.maintainPeriod == null then "break" else "sleep " + toString r.maintainPeriod} ;; (*) exit 1 ;; esac done ''; ExecStop = "${pkgs.miniupnpc}/bin/upnpc -d ${toString r.externalPort} ${r.protocol}"; Restart = "on-failure"; RestartSec = mkDefault r.maintainPeriod; DynamicUser = true; User = users."upnpc".name; }; } r.service ]) ) cfg.redirections); # This enables to match on the uid in the firewall. users.users."upnpc" = { isSystemUser = true; group = groups."upnpc".name; }; users.groups."upnpc" = {}; }; meta.maintainers = with maintainers; [ julm ]; }