{ pkgs, lib, config, ... }:
with lib;
let
- inherit (config.users) users;
+ inherit (config.users) users groups;
cfg = config.services.upnpc;
getInfo = ''
while IFS=: read -r k v; do
'';
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 = {};
- };
- }));
+ options.services.upnpc = {
+ enable = mkEnableOption "UPnP redirections";
+ 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
+ config = mkIf cfg.enable {
+ systemd.services = listToAttrs (map
+ (r:
+ nameValuePair "upnpc-${toString r.internalPort}" (mkMerge [
+ {
+ description = "UPnP ${toString r.internalPort}";
+ after = [ "network-pre.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
}
- ''}
- 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);
+ 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";
+ DynamicUser = true;
+ User = users."upnpc".name;
+ } // lib.optionalAttrs (r.maintainPeriod != null) {
+ RestartSec = mkDefault r.maintainPeriod;
+ };
+ }
+ r.service
+ ])
+ )
+ cfg.redirections);
+
+ environment.systemPackages = [ pkgs.miniupnpc ];
- # This enables to match on the uid in the firewall.
- users.users."upnpc".isSystemUser = true;
-};
-meta.maintainers = with maintainers; [ julm ];
+ # This enables to match on the uid in the firewall.
+ users.users."upnpc" = {
+ isSystemUser = true;
+ group = groups."upnpc".name;
+ };
+ users.groups."upnpc" = { };
+ networking.nftables.ruleset =
+ lib.optionalString (cfg.redirections != [ ]) ''
+ table inet filter {
+ # A set containing the udp port(s) to which SSDP replies are allowed.
+ set upnpc-ssdp {
+ type inet_service
+ timeout 5s
+ }
+ chain input-net {
+ # Create a rule for accepting any SSDP packets going to a remembered port.
+ udp dport @upnpc-ssdp counter accept comment "SSDP answer"
+ }
+ chain output-net {
+ skuid ${users.upnpc.name} \
+ tcp dport ssdp \
+ counter accept \
+ comment "SSDP automatic opening"
+ skuid ${users.upnpc.name} \
+ ip daddr 239.255.255.250 udp dport ssdp \
+ set add udp sport @upnpc-ssdp \
+ comment "SSDP automatic opening"
+ skuid ${users.upnpc.name} \
+ ip daddr 239.255.255.250 udp dport ssdp \
+ counter accept \
+ comment "SSDP"
+ }
+ }
+ '' + lib.optionalString config.networking.enableIPv6 ''
+ table inet filter {
+ chain output-net {
+ skuid ${users.upnpc.name} \
+ ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
+ udp dport ssdp \
+ set add udp sport @upnpc-ssdp \
+ comment "SSDP automatic opening"
+ skuid ${users.upnpc.name} \
+ ip6 daddr { FF02::C, FF05::C, FF08::C, FF0E::C } \
+ udp dport ssdp \
+ counter accept comment "SSDP"
+ }
+ }
+ '';
+ };
+ meta.maintainers = with maintainers; [ julm ];
}