Fixed several long-standing issues with UPnP port forwarding

Fixes local firewall rules to allow discovery of SSDP servers.
No longer disables UPnP forwarding when refreshing fails.
No longer disables UPnP forwarding on system reboot.
Cron job runs every 10 minutes to refresh the router tables more
promptly after the system or router reboots.
Removes the deprecated UPnP "reload" option.
This commit is contained in:
David Buscher 2021-04-03 16:39:20 +01:00
parent 08fbfa2e39
commit 98ad28e290
2 changed files with 37 additions and 63 deletions

View file

@ -1292,7 +1292,6 @@ firewall:
- enable - enable
- disable - disable
- status - status
- reload
nargs: "?" nargs: "?"
default: status default: status
--no-refresh: --no-refresh:

View file

@ -35,7 +35,11 @@ from moulinette.utils.text import prependlines
FIREWALL_FILE = "/etc/yunohost/firewall.yml" FIREWALL_FILE = "/etc/yunohost/firewall.yml"
UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp" UPNP_CRON_JOB = "/etc/cron.d/yunohost-firewall-upnp"
# A UDP port to use for the SSDP discovery phase of UPNP.
# Assigned by IANA to "Fujitsu ICL Terminal Emulator Program A", so no-one else is
# likely to use it (unlike port 1900 which is used by SSDP servers such
# as miniupnpd)
SSDP_CLIENT_PORT = 1901
logger = getActionLogger("yunohost.firewall") logger = getActionLogger("yunohost.firewall")
@ -327,66 +331,57 @@ def firewall_upnp(action="status", no_refresh=False):
""" """
firewall = firewall_list(raw=True) firewall = firewall_list(raw=True)
enabled = firewall["uPnP"]["enabled"]
# Compatibility with previous version if action == "status":
if action == "reload": enabled = firewall["uPnP"]["enabled"]
logger.debug("'reload' action is deprecated and will be removed") elif action == "enable":
try:
# Remove old cron job
os.remove("/etc/cron.d/yunohost-firewall")
except Exception:
pass
action = "status"
no_refresh = False
if action == "status" and no_refresh:
# Only return current state
return {"enabled": enabled}
elif action == "enable" or (enabled and action == "status"):
# Add cron job # Add cron job
with open(UPNP_CRON_JOB, "w+") as f: with open(UPNP_CRON_JOB, "w+") as f:
f.write( f.write(
"*/50 * * * * root " "*/10 * * * * root "
"/usr/bin/yunohost firewall upnp status >>/dev/null\n" "/usr/bin/yunohost firewall upnp status >>/dev/null\n"
) )
# Open port 1900 to receive discovery message
if 1900 not in firewall["ipv4"]["UDP"]:
firewall_allow("UDP", 1900, no_upnp=True, no_reload=True)
if not enabled:
firewall_reload(skip_upnp=True)
enabled = True enabled = True
elif action == "disable" or (not enabled and action == "status"): elif action == "disable":
try: try:
# Remove cron job # Remove cron job
os.remove(UPNP_CRON_JOB) os.remove(UPNP_CRON_JOB)
except Exception: except Exception:
pass pass
enabled = False enabled = False
if action == "status":
no_refresh = True
else: else:
raise YunohostValidationError("action_invalid", action=action) raise YunohostValidationError("action_invalid", action=action)
# Refresh port mapping using UPnP # Refresh port mapping
refresh_success = True
if not no_refresh: if not no_refresh:
upnpc = miniupnpc.UPnP(localport=1) # Open port to receive discovery message
process.run_commands(
["iptables -w -A INPUT -p udp --dport %d -j ACCEPT" % SSDP_CLIENT_PORT],
callback=_on_rule_command_error,
)
upnpc = miniupnpc.UPnP(localport=SSDP_CLIENT_PORT)
upnpc.discoverdelay = 3000 upnpc.discoverdelay = 3000
# Discover UPnP device(s) # Discover UPnP device(s)
logger.debug("discovering UPnP devices...") logger.debug("discovering UPnP devices...")
nb_dev = upnpc.discover() nb_dev = upnpc.discover()
logger.debug("found %d UPnP device(s)", int(nb_dev)) logger.debug("found %d UPnP device(s)", int(nb_dev))
# Close discovery port
process.run_commands(
["iptables -w -D INPUT -p udp --dport %d -j ACCEPT" % SSDP_CLIENT_PORT],
callback=_on_rule_command_error,
)
if nb_dev < 1: if nb_dev < 1:
logger.error(m18n.n("upnp_dev_not_found")) logger.error(m18n.n("upnp_dev_not_found"))
enabled = False refresh_success = False
else: else:
try: try:
# Select UPnP device # Select UPnP device
upnpc.selectigd() upnpc.selectigd()
except Exception: except Exception:
logger.debug("unable to select UPnP device", exc_info=1) logger.debug("unable to select UPnP device", exc_info=1)
enabled = False refresh_success = False
else: else:
# Iterate over ports # Iterate over ports
for protocol in ["TCP", "UDP"]: for protocol in ["TCP", "UDP"]:
@ -398,7 +393,7 @@ def firewall_upnp(action="status", no_refresh=False):
upnpc.deleteportmapping(port, protocol) upnpc.deleteportmapping(port, protocol)
except Exception: except Exception:
pass pass
firewall["uPnP"][protocol + "_TO_CLOSE"] = [] del firewall["uPnP"][protocol + "_TO_CLOSE"]
for port in firewall["uPnP"][protocol]: for port in firewall["uPnP"][protocol]:
# Clean the mapping of this port # Clean the mapping of this port
@ -423,34 +418,17 @@ def firewall_upnp(action="status", no_refresh=False):
logger.debug( logger.debug(
"unable to add port %d using UPnP", port, exc_info=1 "unable to add port %d using UPnP", port, exc_info=1
) )
enabled = False refresh_success = False
if refresh_success:
logger.debug("UPnP port refresh successful")
if action == "enable":
logger.success(m18n.n("upnp_enabled"))
elif action == "disable":
logger.success(m18n.n("upnp_disabled"))
_update_firewall_file(firewall) # Save state always (note that refreshing can change the "TO_CLOSE" states)
firewall["uPnP"]["enabled"] = enabled
if enabled != firewall["uPnP"]["enabled"]: _update_firewall_file(firewall)
firewall = firewall_list(raw=True)
firewall["uPnP"]["enabled"] = enabled
_update_firewall_file(firewall)
if not no_refresh:
# Display success message if needed
if action == "enable" and enabled:
logger.success(m18n.n("upnp_enabled"))
elif action == "disable" and not enabled:
logger.success(m18n.n("upnp_disabled"))
# Make sure to disable UPnP
elif action != "disable" and not enabled:
firewall_upnp("disable", no_refresh=True)
if not enabled and (action == "enable" or 1900 in firewall["ipv4"]["UDP"]):
# Close unused port 1900
firewall_disallow("UDP", 1900, no_reload=True)
if not no_refresh:
firewall_reload(skip_upnp=True)
if action == "enable" and not enabled:
raise YunohostError("upnp_port_open_failed")
return {"enabled": enabled} return {"enabled": enabled}
@ -472,9 +450,6 @@ def firewall_stop():
os.system("ip6tables -F") os.system("ip6tables -F")
os.system("ip6tables -X") os.system("ip6tables -X")
if os.path.exists(UPNP_CRON_JOB):
firewall_upnp("disable")
def _get_ssh_port(default=22): def _get_ssh_port(default=22):
"""Return the SSH port to use """Return the SSH port to use