diff options
-rw-r--r-- | data/configd-include.json | 1 | ||||
-rw-r--r-- | data/templates/firewall/upnpd.conf.tmpl | 172 | ||||
-rw-r--r-- | debian/control | 4 | ||||
-rw-r--r-- | interface-definitions/service_upnp.xml.in | 246 | ||||
-rw-r--r-- | python/vyos/firewall.py | 2 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_service_upnp.py | 71 | ||||
-rwxr-xr-x | src/conf_mode/policy-route.py | 26 | ||||
-rwxr-xr-x | src/conf_mode/service_upnp.py | 155 | ||||
-rw-r--r-- | src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper | 16 | ||||
-rw-r--r-- | src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup | 2 | ||||
-rwxr-xr-x | src/migration-scripts/firewall/6-to-7 | 168 | ||||
-rw-r--r-- | src/systemd/miniupnpd.service | 13 |
12 files changed, 777 insertions, 99 deletions
diff --git a/data/configd-include.json b/data/configd-include.json index 739f2f6c8..c85ab0725 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -55,6 +55,7 @@ "service_mdns-repeater.py", "service_pppoe-server.py", "service_router-advert.py", +"service_upnp.py", "ssh.py", "system-ip.py", "system-ipv6.py", diff --git a/data/templates/firewall/upnpd.conf.tmpl b/data/templates/firewall/upnpd.conf.tmpl new file mode 100644 index 000000000..39cb21373 --- /dev/null +++ b/data/templates/firewall/upnpd.conf.tmpl @@ -0,0 +1,172 @@ +# This is the UPNP configuration file + +# WAN network interface +ext_ifname={{ wan_interface }} +{% if wan_ip is defined %} +# If the WAN interface has several IP addresses, you +# can specify the one to use below +{% for addr in wan_ip %} +ext_ip={{ addr }} +{% endfor %} +{% endif %} + +# LAN network interfaces IPs / networks +{% if listen is defined %} +# There can be multiple listening IPs for SSDP traffic, in that case +# use multiple 'listening_ip=...' lines, one for each network interface. +# It can be IP address or network interface name (ie. "eth0") +# It is mandatory to use the network interface name in order to enable IPv6 +# HTTP is available on all interfaces. +# When MULTIPLE_EXTERNAL_IP is enabled, the external IP +# address associated with the subnet follows. For example: +# listening_ip=192.168.0.1/24 88.22.44.13 +{% for addr in listen %} +{% if addr | is_ipv4 %} +listening_ip={{ addr }} +{% elif addr | is_ipv6 %} +ipv6_listening_ip={{ addr }} +{% else %} +listening_ip={{ addr }} +{% endif %} +{% endfor %} +{% endif %} + +# CAUTION: mixing up WAN and LAN interfaces may introduce security risks! +# Be sure to assign the correct interfaces to LAN and WAN and consider +# implementing UPnP permission rules at the bottom of this configuration file + +# Port for HTTP (descriptions and SOAP) traffic. Set to 0 for autoselect. +#http_port=0 +# Port for HTTPS. Set to 0 for autoselect (default) +#https_port=0 + +# Path to the UNIX socket used to communicate with MiniSSDPd +# If running, MiniSSDPd will manage M-SEARCH answering. +# default is /var/run/minissdpd.sock +#minissdpdsocket=/var/run/minissdpd.sock + +{% if nat_pmp is defined %} +# Enable NAT-PMP support (default is no) +enable_natpmp=yes +{% endif %} + +# Enable UPNP support (default is yes) +enable_upnp=yes + +{% if pcp_lifetime is defined %} +# PCP +# Configure the minimum and maximum lifetime of a port mapping in seconds +# 120s and 86400s (24h) are suggested values from PCP-base +{% if pcp_lifetime.max is defined %} +max_lifetime={{ pcp_lifetime.max }} +{% endif %} +{% if pcp_lifetime.min is defined %} +min_lifetime={{ pcp_lifetime.min }} +{% endif %} +{% endif %} + + +# To enable the next few runtime options, see compile time +# ENABLE_MANUFACTURER_INFO_CONFIGURATION (config.h) + +{% if friendly_name is defined %} +# Name of this service, default is "`uname -s` router" +friendly_name= {{ friendly_name }} +{% endif %} + +# Manufacturer name, default is "`uname -s`" +manufacturer_name=VyOS + +# Manufacturer URL, default is URL of OS vendor +manufacturer_url=https://vyos.io/ + +# Model name, default is "`uname -s` router" +model_name=VyOS Router Model + +# Model description, default is "`uname -s` router" +model_description=Vyos open source enterprise router/firewall operating system + +# Model URL, default is URL of OS vendor +model_url=https://vyos.io/ + +{% if secure_mode is defined %} +# Secure Mode, UPnP clients can only add mappings to their own IP +secure_mode=yes +{% else %} +# Secure Mode, UPnP clients can only add mappings to their own IP +secure_mode=no +{% endif %} + +{% if presentation_url is defined %} +# Default presentation URL is HTTP address on port 80 +# If set to an empty string, no presentationURL element will appear +# in the XML description of the device, which prevents MS Windows +# from displaying an icon in the "Network Connections" panel. +#presentation_url= {{ presentation_url }} +{% endif %} + +# Report system uptime instead of daemon uptime +system_uptime=yes + +# Unused rules cleaning. +# never remove any rule before this threshold for the number +# of redirections is exceeded. default to 20 +clean_ruleset_threshold=10 +# Clean process work interval in seconds. default to 0 (disabled). +# a 600 seconds (10 minutes) interval makes sense +clean_ruleset_interval=600 + +# Anchor name in pf (default is miniupnpd) +anchor=VyOS + +uuid={{ uuid }} + +# Lease file location +lease_file=/config/upnp.leases + +# Daemon's serial and model number when reporting to clients +# (in XML description) +#serial=12345678 +#model_number=1 + +{% if rules is defined %} +# UPnP permission rules +# (allow|deny) (external port range) IP/mask (internal port range) +# A port range is <min port>-<max port> or <port> if there is only +# one port in the range. +# IP/mask format must be nnn.nnn.nnn.nnn/nn +# It is advised to only allow redirection of port >= 1024 +# and end the rule set with "deny 0-65535 0.0.0.0/0 0-65535" +# The following default ruleset allows specific LAN side IP addresses +# to request only ephemeral ports. It is recommended that users +# modify the IP ranges to match their own internal networks, and +# also consider implementing network-specific restrictions +# CAUTION: failure to enforce any rules may permit insecure requests to be made! +{% for rule, config in rules.items() %} +{% if config.disable is defined %} +{{ config.action}} {{ config.external_port_range }} {{ config.ip }} {{ config.internal_port_range }} +{% endif %} +{% endfor %} +{% endif %} + +{% if stun is defined %} +# WAN interface must have public IP address. Otherwise it is behind NAT +# and port forwarding is impossible. In some cases WAN interface can be +# behind unrestricted NAT 1:1 when all incoming traffic is NAT-ed and +# routed to WAN interfaces without any filtering. In this cases miniupnpd +# needs to know public IP address and it can be learnt by asking external +# server via STUN protocol. Following option enable retrieving external +# public IP address from STUN server and detection of NAT type. You need +# to specify also external STUN server in stun_host option below. +# This option is disabled by default. +ext_perform_stun=yes +# Specify STUN server, either hostname or IP address +# Some public STUN servers: +# stun.stunprotocol.org +# stun.sipgate.net +# stun.xten.com +# stun.l.google.com (on non standard port 19302) +ext_stun_host={{ stun.host }} +# Specify STUN UDP port, by default it is standard port 3478. +ext_stun_port={{ stun.port }} +{% endif %} diff --git a/debian/control b/debian/control index 8afdd3300..c53e4d3b8 100644 --- a/debian/control +++ b/debian/control @@ -170,7 +170,9 @@ Depends: wide-dhcpv6-client, wireguard-tools, wireless-regdb, - wpasupplicant (>= 0.6.7) + wpasupplicant (>= 0.6.7), + ndppd, + miniupnpd-nftables Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service_upnp.xml.in new file mode 100644 index 000000000..8d0a14d4e --- /dev/null +++ b/interface-definitions/service_upnp.xml.in @@ -0,0 +1,246 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="service"> + <children> + <node name="upnp" owner="${vyos_conf_scripts_dir}/service_upnp.py"> + <properties> + <help>Universal Plug and Play (UPnP) service</help> + <priority>900</priority> + </properties> + <children> + <leafNode name="friendly-name"> + <properties> + <help>Name of this service</help> + <valueHelp> + <format>txt</format> + <description>Friendly name</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="wan-interface"> + <properties> + <help>WAN network interface (REQUIRE)</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <constraint> + <validator name="interface-name" /> + </constraint> + </properties> + </leafNode> + <leafNode name="wan-ip"> + <properties> + <help>WAN network IP</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address" /> + <validator name="ipv6-address" /> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nat-pmp"> + <properties> + <help>Enable NAT-PMP support</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="secure-mode"> + <properties> + <help>Enable Secure Mode</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="presentation-url"> + <properties> + <help>Presentation Url</help> + <valueHelp> + <format>txt</format> + <description>Presentation Url</description> + </valueHelp> + </properties> + </leafNode> + <node name="pcp-lifetime"> + <properties> + <help>PCP-base lifetime Option</help> + </properties> + <children> + <leafNode name="max"> + <properties> + <help>Max lifetime time</help> + <constraint> + <validator name="numeric" /> + </constraint> + </properties> + </leafNode> + <leafNode name="min"> + <properties> + <help>Min lifetime time</help> + <constraint> + <validator name="numeric" /> + </constraint> + </properties> + </leafNode> + </children> + </node> + <leafNode name="listen"> + <properties> + <help>Local IP addresses for service to listen on</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format><interface></format> + <description>Monitor interface address</description> + </valueHelp> + <valueHelp> + <format>ipv4</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv4-prefix</format> + <description>IP prefix to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IP address to listen for incoming connections</description> + </valueHelp> + <valueHelp> + <format>ipv6-prefix</format> + <description>IP prefix to listen for incoming connections</description> + </valueHelp> + <multi/> + <constraint> + <validator name="interface-name" /> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <node name="stun"> + <properties> + <help>Enable STUN probe support (can be used with NAT 1:1 support for WAN interfaces)</help> + </properties> + <children> + <leafNode name="host"> + <properties> + <help>The STUN server address</help> + <valueHelp> + <format>txt</format> + <description>The STUN server host address</description> + </valueHelp> + <valueHelp> + <format>stun.stunprotocol.org</format> + <description>stunprotocol</description> + </valueHelp> + <valueHelp> + <format>stun.sipgate.net</format> + <description>sipgate</description> + </valueHelp> + <valueHelp> + <format>stun.xten.com</format> + <description>xten</description> + </valueHelp> + <valueHelp> + <format>txt</format> + <description>other STUN Server</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="port"> + <properties> + <help>The STUN server port</help> + <valueHelp> + <format>txt</format> + <description>The STUN server port</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> + <tagNode name="rules"> + <properties> + <help>UPnP Rule</help> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <children> + <leafNode name="disable"> + <properties> + <help>Disable Rule</help> + <valueless /> + </properties> + </leafNode> + <leafNode name="external-port-range"> + <properties> + <help>Port range (REQUIRE)</help> + <valueHelp> + <format><port></format> + <description>single port</description> + </valueHelp> + <valueHelp> + <format><portN>-<portM></format> + <description>Port range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="port-range"/> + </constraint> + </properties> + </leafNode> + <leafNode name="internal-port-range"> + <properties> + <help>Port range (REQUIRE)</help> + <valueHelp> + <format><port></format> + <description>single port</description> + </valueHelp> + <valueHelp> + <format><portN>-<portM></format> + <description>Port range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="port-range"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ip"> + <properties> + <help>The IP to which this rule applies (REQUIRE)</help> + <valueHelp> + <format>ipv4</format> + <description>The IPv4 to which this rule applies</description> + </valueHelp> + <constraint> + <validator name="ipv4-address" /> + </constraint> + </properties> + </leafNode> + <leafNode name="action"> + <properties> + <help>Actions against the rule (REQUIRE)</help> + <completionHelp> + <list>allow deny</list> + </completionHelp> + <constraint> + <regex>^(allow|deny)$</regex> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 4993d855e..a2e133217 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -190,7 +190,7 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): def parse_tcp_flags(flags): include = [flag for flag in flags if flag != 'not'] - exclude = flags['not'].keys() if 'not' in flags else [] + exclude = list(flags['not']) if 'not' in flags else [] return f'tcp flags & ({"|".join(include + exclude)}) == {"|".join(include)}' def parse_time(time): diff --git a/smoketest/scripts/cli/test_service_upnp.py b/smoketest/scripts/cli/test_service_upnp.py new file mode 100755 index 000000000..9fbbdaff9 --- /dev/null +++ b/smoketest/scripts/cli/test_service_upnp.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM + +from vyos.configsession import ConfigSession +from vyos.util import read_file +from vyos.util import process_named_running + +UPNP_CONF = '/run/upnp/miniupnp.conf' +interface = 'eth0' +base_path = ['service', 'upnp'] +address_base = ['interfaces', 'ethernet', interface, 'address'] + +class TestServiceUPnP(VyOSUnitTestSHIM.TestCase): + def tearDown(self): + self.cli_delete(address_base) + self.cli_delete(base_path) + self.cli_commit() + + def test_ipv4_base(self): + self.cli_set(address_base + ['100.64.0.1/24']) + self.cli_set(base_path + ['nat-pmp']) + self.cli_set(base_path + ['wan-interface', interface]) + self.cli_set(base_path + ['listen', interface]) + self.cli_commit() + + config = read_file(UPNP_CONF) + self.assertIn(f'ext_ifname={interface}', config) + self.assertIn(f'listening_ip={interface}', config) + self.assertIn(f'enable_natpmp=yes', config) + self.assertIn(f'enable_upnp=yes', config) + + # Check for running process + self.assertTrue(process_named_running('miniupnpd')) + + def test_ipv6_base(self): + self.cli_set(address_base + ['2001:db8::1/64']) + self.cli_set(base_path + ['nat-pmp']) + self.cli_set(base_path + ['wan-interface', interface]) + self.cli_set(base_path + ['listen', interface]) + self.cli_set(base_path + ['listen', '2001:db8::1']) + self.cli_commit() + + config = read_file(UPNP_CONF) + self.assertIn(f'ext_ifname={interface}', config) + self.assertIn(f'listening_ip={interface}', config) + self.assertIn(f'enable_natpmp=yes', config) + self.assertIn(f'enable_upnp=yes', config) + + # Check for running process + self.assertTrue(process_named_running('miniupnpd')) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/src/conf_mode/policy-route.py b/src/conf_mode/policy-route.py index ee5197af0..7dcab4b58 100755 --- a/src/conf_mode/policy-route.py +++ b/src/conf_mode/policy-route.py @@ -205,6 +205,7 @@ def generate(policy): def apply_table_marks(policy): for route in ['route', 'route6']: if route in policy: + cmd_str = 'ip' if route == 'route' else 'ip -6' for name, pol_conf in policy[route].items(): if 'rule' in pol_conf: for rule_id, rule_conf in pol_conf['rule'].items(): @@ -213,20 +214,21 @@ def apply_table_marks(policy): if set_table == 'main': set_table = '254' table_mark = mark_offset - int(set_table) - cmd(f'ip rule add fwmark {table_mark} table {set_table}') + cmd(f'{cmd_str} rule add pref {set_table} fwmark {table_mark} table {set_table}') def cleanup_table_marks(): - json_rules = cmd('ip -j -N rule list') - rules = loads(json_rules) - for rule in rules: - if 'fwmark' not in rule or 'table' not in rule: - continue - fwmark = rule['fwmark'] - table = int(rule['table']) - if fwmark[:2] == '0x': - fwmark = int(fwmark, 16) - if (int(fwmark) == (mark_offset - table)): - cmd(f'ip rule del fwmark {fwmark} table {table}') + for cmd_str in ['ip', 'ip -6']: + json_rules = cmd(f'{cmd_str} -j -N rule list') + rules = loads(json_rules) + for rule in rules: + if 'fwmark' not in rule or 'table' not in rule: + continue + fwmark = rule['fwmark'] + table = int(rule['table']) + if fwmark[:2] == '0x': + fwmark = int(fwmark, 16) + if (int(fwmark) == (mark_offset - table)): + cmd(f'{cmd_str} rule del fwmark {fwmark} table {table}') def apply(policy): install_result = run(f'nft -f {nftables_conf}') diff --git a/src/conf_mode/service_upnp.py b/src/conf_mode/service_upnp.py new file mode 100755 index 000000000..638296f45 --- /dev/null +++ b/src/conf_mode/service_upnp.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from sys import exit +import uuid +import netifaces +from ipaddress import IPv4Network +from ipaddress import IPv6Network + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import dict_search +from vyos.configdict import get_interface_dict +from vyos.configverify import verify_vrf +from vyos.util import call +from vyos.template import render +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/run/upnp/miniupnp.conf' + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + base = ['service', 'upnp'] + upnpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + + if not upnpd: + return None + + if dict_search('rule', upnpd): + default_member_values = defaults(base + ['rule']) + for rule,rule_config in upnpd['rule'].items(): + upnpd['rule'][rule] = dict_merge(default_member_values, upnpd['rule'][rule]) + + uuidgen = uuid.uuid1() + upnpd.update({'uuid': uuidgen}) + + return upnpd + +def get_all_interface_addr(prefix, filter_dev, filter_family): + list_addr = [] + interfaces = netifaces.interfaces() + + for interface in interfaces: + if filter_dev and interface in filter_dev: + continue + addrs = netifaces.ifaddresses(interface) + if netifaces.AF_INET in addrs.keys(): + if netifaces.AF_INET in filter_family: + for addr in addrs[netifaces.AF_INET]: + if prefix: + # we need to manually assemble a list of IPv4 address/prefix + prefix = '/' + \ + str(IPv4Network('0.0.0.0/' + addr['netmask']).prefixlen) + list_addr.append(addr['addr'] + prefix) + else: + list_addr.append(addr['addr']) + if netifaces.AF_INET6 in addrs.keys(): + if netifaces.AF_INET6 in filter_family: + for addr in addrs[netifaces.AF_INET6]: + if prefix: + # we need to manually assemble a list of IPv4 address/prefix + bits = bin(int(addr['netmask'].replace(':', '').split('/')[0], 16)).count('1') + prefix = '/' + str(bits) + list_addr.append(addr['addr'] + prefix) + else: + list_addr.append(addr['addr']) + + return list_addr + +def verify(upnpd): + if not upnpd: + return None + + if 'wan_interface' not in upnpd: + raise ConfigError('To enable UPNP, you must have the "wan-interface" option!') + + if dict_search('rules', upnpd): + for rule,rule_config in upnpd['rule'].items(): + for option in ['external_port_range', 'internal_port_range', 'ip', 'action']: + if option not in rule_config: + raise ConfigError(f'A UPNP rule must have an "{option}" option!') + + if dict_search('stun', upnpd): + for option in ['host', 'port']: + if option not in upnpd['stun']: + raise ConfigError(f'A UPNP stun support must have an "{option}" option!') + + # Check the validity of the IP address + listen_dev = [] + system_addrs_cidr = get_all_interface_addr(True, [], [netifaces.AF_INET, netifaces.AF_INET6]) + system_addrs = get_all_interface_addr(False, [], [netifaces.AF_INET, netifaces.AF_INET6]) + for listen_if_or_addr in upnpd['listen']: + if listen_if_or_addr not in netifaces.interfaces(): + listen_dev.append(listen_if_or_addr) + if (listen_if_or_addr not in system_addrs) and (listen_if_or_addr not in system_addrs_cidr) and (listen_if_or_addr not in netifaces.interfaces()): + if is_ipv4(listen_if_or_addr) and IPv4Network(listen_if_or_addr).is_multicast: + raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + if is_ipv6(listen_if_or_addr) and IPv6Network(listen_if_or_addr).is_multicast: + raise ConfigError(f'The address "{listen_if_or_addr}" is an address that is not allowed to listen on. It is not an interface address nor a multicast address!') + + system_listening_dev_addrs_cidr = get_all_interface_addr(True, listen_dev, [netifaces.AF_INET6]) + system_listening_dev_addrs = get_all_interface_addr(False, listen_dev, [netifaces.AF_INET6]) + for listen_if_or_addr in upnpd['listen']: + if listen_if_or_addr not in netifaces.interfaces() and (listen_if_or_addr not in system_listening_dev_addrs_cidr) and (listen_if_or_addr not in system_listening_dev_addrs) and is_ipv6(listen_if_or_addr) and (not IPv6Network(listen_if_or_addr).is_multicast): + raise ConfigError(f'{listen_if_or_addr} must listen on the interface of the network card') + +def generate(upnpd): + if not upnpd: + return None + + if os.path.isfile(config_file): + os.unlink(config_file) + + render(config_file, 'firewall/upnpd.conf.tmpl', upnpd) + +def apply(upnpd): + if not upnpd: + # Stop the UPNP service + call('systemctl stop miniupnpd.service') + else: + # Start the UPNP service + call('systemctl restart miniupnpd.service') + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper index 74a7e83bf..9d5505758 100644 --- a/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper +++ b/src/etc/dhcp/dhclient-enter-hooks.d/03-vyos-ipwrapper @@ -4,7 +4,7 @@ IF_METRIC=${IF_METRIC:-210} # Check if interface is inside a VRF -VRF_OPTION=$(/usr/sbin/ip -j -d link show ${interface} | awk '{if(match($0, /.*"master":"(\w+)".*"info_slave_kind":"vrf"/, IFACE_DETAILS)) printf("vrf %s", IFACE_DETAILS[1])}') +VRF_OPTION=$(ip -j -d link show ${interface} | awk '{if(match($0, /.*"master":"(\w+)".*"info_slave_kind":"vrf"/, IFACE_DETAILS)) printf("vrf %s", IFACE_DETAILS[1])}') # get status of FRR function frr_alive () { @@ -66,9 +66,9 @@ function iptovtysh () { # delete the same route from kernel before adding new one function delroute () { logmsg info "Checking if the route presented in kernel: $@ $VRF_OPTION" - if /usr/sbin/ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then - logmsg info "Deleting IP route: \"/usr/sbin/ip route del $@ $VRF_OPTION\"" - /usr/sbin/ip route del $@ $VRF_OPTION + if ip route show $@ $VRF_OPTION | grep -qx "$1 " ; then + logmsg info "Deleting IP route: \"ip route del $@ $VRF_OPTION\"" + ip route del $@ $VRF_OPTION fi } @@ -76,8 +76,8 @@ function delroute () { function ip () { # pass comand to system `ip` if this is not related to routes change if [ "$2" != "route" ] ; then - logmsg info "Passing command to /usr/sbin/ip: \"$@\"" - /usr/sbin/ip $@ + logmsg info "Passing command to iproute2: \"$@\"" + ip $@ else # if we want to work with routes, try to use FRR first if frr_alive ; then @@ -87,8 +87,8 @@ function ip () { vtysh -c "conf t" -c "$VTYSH_CMD" else # add ip route to kernel - logmsg info "Modifying routes in kernel: \"/usr/sbin/ip $@\"" - /usr/sbin/ip $@ $VRF_OPTION + logmsg info "Modifying routes in kernel: \"ip $@\"" + ip $@ $VRF_OPTION fi fi } diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup index ad6a1d5eb..a6989441b 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup +++ b/src/etc/dhcp/dhclient-exit-hooks.d/01-vyos-cleanup @@ -1,7 +1,7 @@ ## ## VyOS cleanup ## -# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via /usr/sbin/ip or vtysh, according to the system state +# NOTE: here we use 'ip' wrapper, therefore a route will be actually deleted via ip or vtysh, according to the system state hostsd_client="/usr/bin/vyos-hostsd-client" hostsd_changes= # check vyos-hostsd status diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7 index cc3a9b559..efc901530 100755 --- a/src/migration-scripts/firewall/6-to-7 +++ b/src/migration-scripts/firewall/6-to-7 @@ -100,87 +100,103 @@ icmpv6_translations = { if config.exists(base + ['name']): for name in config.list_nodes(base + ['name']): - if config.exists(base + ['name', name, 'rule']): - for rule in config.list_nodes(base + ['name', name, 'rule']): - rule_time = base + ['name', name, 'rule', rule, 'time'] - rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags'] - rule_icmp = base + ['name', name, 'rule', rule, 'icmp'] - - if config.exists(rule_time + ['monthdays']): - config.delete(rule_time + ['monthdays']) - - if config.exists(rule_time + ['utc']): - config.delete(rule_time + ['utc']) - - if config.exists(rule_tcp_flags): - tmp = config.return_value(rule_tcp_flags) - config.delete(rule_tcp_flags) - for flag in tmp.split(","): - if flag[0] == '!': - config.set(rule_tcp_flags + ['not', flag[1:].lower()]) - else: - config.set(rule_tcp_flags + [flag.lower()]) - - if config.exists(rule_icmp + ['type-name']): - tmp = config.return_value(rule_icmp + ['type-name']) - if tmp in icmp_remove: + if not config.exists(base + ['name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['name', name, 'rule']): + rule_time = base + ['name', name, 'rule', rule, 'time'] + rule_tcp_flags = base + ['name', name, 'rule', rule, 'tcp', 'flags'] + rule_icmp = base + ['name', name, 'rule', rule, 'icmp'] + + if config.exists(rule_time + ['monthdays']): + config.delete(rule_time + ['monthdays']) + + if config.exists(rule_time + ['utc']): + config.delete(rule_time + ['utc']) + + if config.exists(rule_tcp_flags): + tmp = config.return_value(rule_tcp_flags) + config.delete(rule_tcp_flags) + for flag in tmp.split(","): + if flag[0] == '!': + config.set(rule_tcp_flags + ['not', flag[1:].lower()]) + else: + config.set(rule_tcp_flags + [flag.lower()]) + + if config.exists(rule_icmp + ['type-name']): + tmp = config.return_value(rule_icmp + ['type-name']) + if tmp in icmp_remove: + config.delete(rule_icmp + ['type-name']) + elif tmp in icmp_translations: + translate = icmp_translations[tmp] + if isinstance(translate, str): + config.set(rule_icmp + ['type-name'], value=translate) + elif isinstance(translate, list): config.delete(rule_icmp + ['type-name']) - elif tmp in icmp_translations: - translate = icmp_translations[tmp] - if isinstance(translate, str): - config.set(rule_icmp + ['type-name'], value=translate) - elif isinstance(translate, list): - config.delete(rule_icmp + ['type-name']) - config.set(rule_icmp + ['type'], value=translate[0]) - config.set(rule_icmp + ['code'], value=translate[1]) + config.set(rule_icmp + ['type'], value=translate[0]) + config.set(rule_icmp + ['code'], value=translate[1]) + + for src_dst in ['destination', 'source']: + pg_base = base + ['name', name, 'rule', rule, src_dst, 'group', 'port-group'] + proto_base = base + ['name', name, 'rule', rule, 'protocol'] + if config.exists(pg_base) and not config.exists(proto_base): + config.set(proto_base, value='tcp_udp') if config.exists(base + ['ipv6-name']): for name in config.list_nodes(base + ['ipv6-name']): - if config.exists(base + ['ipv6-name', name, 'rule']): - for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): - rule_time = base + ['ipv6-name', name, 'rule', rule, 'time'] - rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags'] - rule_icmp = base + ['ipv6-name', name, 'rule', rule, 'icmpv6'] - - if config.exists(rule_time + ['monthdays']): - config.delete(rule_time + ['monthdays']) - - if config.exists(rule_time + ['utc']): - config.delete(rule_time + ['utc']) - - if config.exists(rule_tcp_flags): - tmp = config.return_value(rule_tcp_flags) - config.delete(rule_tcp_flags) - for flag in tmp.split(","): - if flag[0] == '!': - config.set(rule_tcp_flags + ['not', flag[1:].lower()]) - else: - config.set(rule_tcp_flags + [flag.lower()]) - - if config.exists(base + ['ipv6-name', name, 'rule', rule, 'protocol']): - tmp = config.return_value(base + ['ipv6-name', name, 'rule', rule, 'protocol']) - if tmp == 'icmpv6': - config.set(base + ['ipv6-name', name, 'rule', rule, 'protocol'], value='ipv6-icmp') - - if config.exists(rule_icmp + ['type']): - tmp = config.return_value(rule_icmp + ['type']) - type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) - - if type_code_match: - config.set(rule_icmp + ['type'], value=type_code_match[1]) - config.set(rule_icmp + ['code'], value=type_code_match[2]) - elif tmp in icmpv6_remove: - config.delete(rule_icmp + ['type']) - elif tmp in icmpv6_translations: - translate = icmpv6_translations[tmp] - if isinstance(translate, str): - config.delete(rule_icmp + ['type']) - config.set(rule_icmp + ['type-name'], value=translate) - elif isinstance(translate, list): - config.set(rule_icmp + ['type'], value=translate[0]) - config.set(rule_icmp + ['code'], value=translate[1]) + if not config.exists(base + ['ipv6-name', name, 'rule']): + continue + + for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']): + rule_time = base + ['ipv6-name', name, 'rule', rule, 'time'] + rule_tcp_flags = base + ['ipv6-name', name, 'rule', rule, 'tcp', 'flags'] + rule_icmp = base + ['ipv6-name', name, 'rule', rule, 'icmpv6'] + + if config.exists(rule_time + ['monthdays']): + config.delete(rule_time + ['monthdays']) + + if config.exists(rule_time + ['utc']): + config.delete(rule_time + ['utc']) + + if config.exists(rule_tcp_flags): + tmp = config.return_value(rule_tcp_flags) + config.delete(rule_tcp_flags) + for flag in tmp.split(","): + if flag[0] == '!': + config.set(rule_tcp_flags + ['not', flag[1:].lower()]) else: - config.rename(rule_icmp + ['type'], 'type-name') + config.set(rule_tcp_flags + [flag.lower()]) + + if config.exists(base + ['ipv6-name', name, 'rule', rule, 'protocol']): + tmp = config.return_value(base + ['ipv6-name', name, 'rule', rule, 'protocol']) + if tmp == 'icmpv6': + config.set(base + ['ipv6-name', name, 'rule', rule, 'protocol'], value='ipv6-icmp') + + if config.exists(rule_icmp + ['type']): + tmp = config.return_value(rule_icmp + ['type']) + type_code_match = re.match(r'^(\d+)/(\d+)$', tmp) + + if type_code_match: + config.set(rule_icmp + ['type'], value=type_code_match[1]) + config.set(rule_icmp + ['code'], value=type_code_match[2]) + elif tmp in icmpv6_remove: + config.delete(rule_icmp + ['type']) + elif tmp in icmpv6_translations: + translate = icmpv6_translations[tmp] + if isinstance(translate, str): + config.delete(rule_icmp + ['type']) + config.set(rule_icmp + ['type-name'], value=translate) + elif isinstance(translate, list): + config.set(rule_icmp + ['type'], value=translate[0]) + config.set(rule_icmp + ['code'], value=translate[1]) + else: + config.rename(rule_icmp + ['type'], 'type-name') + + for src_dst in ['destination', 'source']: + pg_base = base + ['ipv6-name', name, 'rule', rule, src_dst, 'group', 'port-group'] + proto_base = base + ['ipv6-name', name, 'rule', rule, 'protocol'] + if config.exists(pg_base) and not config.exists(proto_base): + config.set(proto_base, value='tcp_udp') try: with open(file_name, 'w') as f: diff --git a/src/systemd/miniupnpd.service b/src/systemd/miniupnpd.service new file mode 100644 index 000000000..51cb2eed8 --- /dev/null +++ b/src/systemd/miniupnpd.service @@ -0,0 +1,13 @@ +[Unit] +Description=UPnP service +ConditionPathExists=/run/upnp/miniupnp.conf +After=vyos-router.service +StartLimitIntervalSec=0 + +[Service] +WorkingDirectory=/run/upnp +Type=simple +ExecStart=/usr/sbin/miniupnpd -d -f /run/upnp/miniupnp.conf +PrivateTmp=yes +PIDFile=/run/miniupnpd.pid +Restart=on-failure |