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  | 
