UPnP and NAT-PMP: ports the devices open for themselves¶
Game consoles, video calls and peer-to-peer programs ask the router to
forward a port to them, on their own, instead of waiting for an admin's
port forward: UPnP IGD (UPnP's Internet Gateway Device), NAT-PMP and its
successor PCP. pfSense does it with miniupnpd; so does Stronghold, as a
service function (upnp), off unless the config turns it on.
Status (20 October 2026): done, as below.
Config¶
# firewall/nat.yaml
upnp:
networks: [lan] # the inside networks whose devices may ask
wan: wan # where the ports are opened; default the only WAN
protocols: [upnp, nat-pmp] # the default: both (nat-pmp is NAT-PMP and PCP)
ports: 1024-65535 # the external ports a device may have (the default)
max-lifetime: 86400 # longest NAT-PMP/PCP mapping before renewal, seconds
private-wan: false # the WAN's own address is private: map anyway
It is part of the firewall section, edited with NAT (Firewall › NAT, "UPnP
and NAT-PMP") and applied with it, under the same confirm window. Turning
the function off (Features, stronghold functions) removes the section.
How¶
- miniupnpd, Fedora's (
miniupnpd, BSD; built for nftables), in a container of its own (stronghold-upnp: host network,NET_ADMINand nothing else, as a container under SELinux), or on the host from the package (runtime: host), where it has no domain of its own and runs unconfined. strongholdd writes/etc/miniupnpd/miniupnpd.conf. - Its mappings are DNAT rules in a table of its own,
inet stronghold_upnp(chainsprerouting_miniupnpd,postrouting_miniupnpd,miniupnpd), which strongholdd makes before miniupnpd starts (it only adds to chains that are there) and takes away when it is off. Applying the config again keeps them; a new start (its config changed) begins with them empty, and the devices ask again, as they do anyway. - firewalld lets them through: it accepts translated connections
(
ct status dnat), as for the port forwards; there is no filter of miniupnpd's own to keep in step. - Stronghold's NAT table comes first (priority
dstnat - 10, the mappings'dstnat), and the first NAT rule to match a connection decides: a port forward of the config always wins over a device's. - What a device may have (miniupnpd's permission rules, the first match decides):
- never a port the box answers on that WAN (its own rules and the user's from the WAN's zone to the gateway: SSH, Cockpit, WireGuard, public DNS, the reverse proxy, ...) or forwards there: a mapping would take their connections;
- then
ports, to its own address only (secure_mode), from the networks listed; - nothing else.
- The devices reach it on their networks only (system rules in each network's zone to the gateway, from its subnet): SSDP (UDP 1900, multicast), its HTTP (TCP 2189, a fixed port: the descriptions and requests) for UPnP, UDP 5351 for NAT-PMP and PCP.
- A private WAN address (another router's NAT in front, CGNAT): by
default miniupnpd maps nothing, since the outside can't reach it.
private-wanmaps anyway, for a router in front that forwards everything to the box (or for the lab). - IPv4 only. IPv6 pinholes would be accepts in miniupnpd's own table, which firewalld's filter doesn't heed: not yet.
Status and tools¶
GET /v1/nat/upnp,stronghold upnp: whether it runs, on which WAN, and each mapping (protocol, WAN port, the device and its port), read from its table (the names the devices give them are miniupnpd's own: UPnP requests in Troubleshoot has them).- Firewall › NAT lists them; Overview has a UPnP tile (the ports open); Troubleshoot has UPnP requests (miniupnpd's log) and UPnP's port mappings in nftables.
In the lab¶
0.4.0 on the package-mode VM (stronghold-proxy: LAN 192.168.74.2, WAN
by DHCP on libvirt's default, a private address, so private-wan), a
client on its LAN (192.168.74.50, a container with miniupnpc and
libnatpmp), the development host on the WAN side (192.168.122.1):
- Turned on: refused until the function was installed (
stronghold functions install upnp), then its container (stronghold-upnp, the lab registry's image), its table, miniupnpd onenp2s0. upnpc -lfound the box by SSDP (rootDesc.xmlon 2189);upnpc -amade 18000 → the client's 8000, and a connection from the host to the WAN address on 18000 reached the client's listener. Port 22, the forwarded 8443 and the reverse proxy's 80 and 443 were refused (606), as was a mapping for another address (secure mode).- NAT-PMP: a mapping of 19000 (a request by hand: natpmpc gives up on a private public address itself); one asked for 546 (DHCPv6's, the box's) got another port (1024) instead, as NAT-PMP does.
- The same config again kept the mappings; a change started miniupnpd fresh, without them.
- On the host (
runtime: host, Fedora's package by PackageKit): Fedora's unit without its scripts (noinet filterof theirs), the same mapping and connection, miniupnpd unconfined as expected. - Off (
disabled: true): stopped, its table, drop-in and firewall rules gone. No SELinux denials.
Found on the way: miniupnpd matches the IP protocol by the header's byte
(@nh,72,8), which the status didn't read (a test now has the lab's
rule), and keeps the devices' descriptions to itself.
Not yet¶
- In an HA pair: the mappings would be the active node's alone and lost on a failover (miniupnpd keeps them in memory; Fedora's build has no lease file): refused for now.
- IPv6 pinholes (PCP, UPnP's WANIPv6FirewallControl).
- Mappings on more than one WAN (miniupnpd has one external interface).
- Per-device rules (which hosts may map which ports), and removing a mapping from the UI.
- Reflection for the mappings: a device inside reaching another's mapping at the WAN address.
- On a podman host: the mappings go in the host's nftables, which aren't Stronghold's there; refused.