diff options
32 files changed, 435 insertions, 161 deletions
diff --git a/data/templates/firewall/nftables-policy.j2 b/data/templates/firewall/nftables-policy.j2 index 281525407..40118930b 100644 --- a/data/templates/firewall/nftables-policy.j2 +++ b/data/templates/firewall/nftables-policy.j2 @@ -25,7 +25,6 @@ table ip mangle { {{ rule_conf | nft_rule(route_text, rule_id, 'ip') }} {% endfor %} {% endif %} - {{ conf | nft_default_rule(route_text) }} } {% endfor %} {% endif %} @@ -50,7 +49,6 @@ table ip6 mangle { {{ rule_conf | nft_rule(route_text, rule_id, 'ip6') }} {% endfor %} {% endif %} - {{ conf | nft_default_rule(route_text) }} } {% endfor %} {% endif %} diff --git a/data/templates/firewall/nftables-static-nat.j2 b/data/templates/firewall/nftables-static-nat.j2 new file mode 100644 index 000000000..d3c43858f --- /dev/null +++ b/data/templates/firewall/nftables-static-nat.j2 @@ -0,0 +1,115 @@ +#!/usr/sbin/nft -f + +{% macro nat_rule(rule, config, chain) %} +{% set comment = '' %} +{% set base_log = '' %} + +{% if chain is vyos_defined('PREROUTING') %} +{% set comment = 'STATIC-NAT-' ~ rule %} +{% set base_log = '[NAT-DST-' ~ rule %} +{% set interface = ' iifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %} +{% if config.translation.address is vyos_defined %} +{# support 1:1 network translation #} +{% if config.translation.address | is_ip_network %} +{% set trns_addr = 'dnat ip prefix to ip daddr map { ' ~ config.destination.address ~ ' : ' ~ config.translation.address ~ ' }' %} +{# we can now clear out the dst_addr part as it's already covered in aboves map #} +{% else %} +{% set dst_addr = 'ip daddr ' ~ config.destination.address if config.destination.address is vyos_defined %} +{% set trns_addr = 'dnat to ' ~ config.translation.address %} +{% endif %} +{% endif %} +{% elif chain is vyos_defined('POSTROUTING') %} +{% set comment = 'STATIC-NAT-' ~ rule %} +{% set base_log = '[NAT-SRC-' ~ rule %} +{% set interface = ' oifname "' ~ config.inbound_interface ~ '"' if config.inbound_interface is vyos_defined and config.inbound_interface is not vyos_defined('any') else '' %} +{% if config.translation.address is vyos_defined %} +{# support 1:1 network translation #} +{% if config.translation.address | is_ip_network %} +{% set trns_addr = 'snat ip prefix to ip saddr map { ' ~ config.translation.address ~ ' : ' ~ config.destination.address ~ ' }' %} +{# we can now clear out the src_addr part as it's already covered in aboves map #} +{% else %} +{% set src_addr = 'ip saddr ' ~ config.translation.address if config.translation.address is vyos_defined %} +{% set trns_addr = 'snat to ' ~ config.destination.address %} +{% endif %} +{% endif %} +{% endif %} + +{% if config.exclude is vyos_defined %} +{# rule has been marked as 'exclude' thus we simply return here #} +{% set trns_addr = 'return' %} +{% set trns_port = '' %} +{% endif %} + +{% if config.translation.options is vyos_defined %} +{% if config.translation.options.address_mapping is vyos_defined('persistent') %} +{% set trns_opts_addr = 'persistent' %} +{% endif %} +{% if config.translation.options.port_mapping is vyos_defined('random') %} +{% set trns_opts_port = 'random' %} +{% elif config.translation.options.port_mapping is vyos_defined('fully-random') %} +{% set trns_opts_port = 'fully-random' %} +{% endif %} +{% endif %} + +{% if trns_opts_addr is vyos_defined and trns_opts_port is vyos_defined %} +{% set trns_opts = trns_opts_addr ~ ',' ~ trns_opts_port %} +{% elif trns_opts_addr is vyos_defined %} +{% set trns_opts = trns_opts_addr %} +{% elif trns_opts_port is vyos_defined %} +{% set trns_opts = trns_opts_port %} +{% endif %} + +{% set output = 'add rule ip vyos_static_nat ' ~ chain ~ interface %} + +{% if dst_addr is vyos_defined %} +{% set output = output ~ ' ' ~ dst_addr %} +{% endif %} +{% if src_addr is vyos_defined %} +{% set output = output ~ ' ' ~ src_addr %} +{% endif %} + +{# Count packets #} +{% set output = output ~ ' counter' %} +{# Special handling of log option, we must repeat the entire rule before the #} +{# NAT translation options are added, this is essential #} +{% if log is vyos_defined %} +{% set log_output = output ~ ' log prefix "' ~ log ~ '" comment "' ~ comment ~ '"' %} +{% endif %} +{% if trns_addr is vyos_defined %} +{% set output = output ~ ' ' ~ trns_addr %} +{% endif %} + +{% if trns_opts is vyos_defined %} +{% set output = output ~ ' ' ~ trns_opts %} +{% endif %} +{% if comment is vyos_defined %} +{% set output = output ~ ' comment "' ~ comment ~ '"' %} +{% endif %} +{{ log_output if log_output is vyos_defined }} +{{ output }} +{% endmacro %} + +# Start with clean STATIC NAT chains +flush chain ip vyos_static_nat PREROUTING +flush chain ip vyos_static_nat POSTROUTING + +{# NAT if enabled - add targets to nftables #} + +# +# Destination NAT rules build up here +# +add rule ip vyos_static_nat PREROUTING counter jump VYOS_PRE_DNAT_HOOK +{% if static.rule is vyos_defined %} +{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{{ nat_rule(rule, config, 'PREROUTING') }} +{% endfor %} +{% endif %} +# +# Source NAT rules build up here +# +add rule ip vyos_static_nat POSTROUTING counter jump VYOS_PRE_SNAT_HOOK +{% if static.rule is vyos_defined %} +{% for rule, config in static.rule.items() if config.disable is not vyos_defined %} +{{ nat_rule(rule, config, 'POSTROUTING') }} +{% endfor %} +{% endif %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index b91fed615..5971e1bbc 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -181,6 +181,26 @@ table ip nat { } } +table ip vyos_static_nat { + chain PREROUTING { + type nat hook prerouting priority -100; policy accept; + counter jump VYOS_PRE_DNAT_HOOK + } + + chain POSTROUTING { + type nat hook postrouting priority 100; policy accept; + counter jump VYOS_PRE_SNAT_HOOK + } + + chain VYOS_PRE_DNAT_HOOK { + return + } + + chain VYOS_PRE_SNAT_HOOK { + return + } +} + table ip6 nat { chain PREROUTING { type nat hook prerouting priority -100; policy accept; diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in index ed84acbb7..cca3c0f7d 100644 --- a/interface-definitions/firewall.xml.in +++ b/interface-definitions/firewall.xml.in @@ -342,8 +342,8 @@ </constraint> </properties> <children> - #include <include/firewall/name-default-action.xml.i> - #include <include/firewall/name-default-log.xml.i> + #include <include/firewall/default-action.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> @@ -434,7 +434,7 @@ <children> <leafNode name="code"> <properties> - <help>ICMPv6 code (0-255)</help> + <help>ICMPv6 code</help> <valueHelp> <format>u32:0-255</format> <description>ICMPv6 code (0-255)</description> @@ -446,7 +446,7 @@ </leafNode> <leafNode name="type"> <properties> - <help>ICMPv6 type (0-255)</help> + <help>ICMPv6 type</help> <valueHelp> <format>u32:0-255</format> <description>ICMPv6 type (0-255)</description> @@ -531,8 +531,8 @@ </constraint> </properties> <children> - #include <include/firewall/name-default-action.xml.i> - #include <include/firewall/name-default-log.xml.i> + #include <include/firewall/default-action.xml.i> + #include <include/firewall/enable-default-log.xml.i> #include <include/generic-description.xml.i> <tagNode name="rule"> <properties> @@ -580,7 +580,7 @@ <children> <leafNode name="code"> <properties> - <help>ICMP code (0-255)</help> + <help>ICMP code</help> <valueHelp> <format>u32:0-255</format> <description>ICMP code (0-255)</description> @@ -592,7 +592,7 @@ </leafNode> <leafNode name="type"> <properties> - <help>ICMP type (0-255)</help> + <help>ICMP type</help> <valueHelp> <format>u32:0-255</format> <description>ICMP type (0-255)</description> diff --git a/interface-definitions/include/firewall/name-default-action.xml.i b/interface-definitions/include/firewall/default-action.xml.i index 512b0296f..92a2fcaaf 100644 --- a/interface-definitions/include/firewall/name-default-action.xml.i +++ b/interface-definitions/include/firewall/default-action.xml.i @@ -1,4 +1,4 @@ -<!-- include start from firewall/name-default-action.xml.i --> +<!-- include start from firewall/default-action.xml.i --> <leafNode name="default-action"> <properties> <help>Default-action for rule-set</help> @@ -21,5 +21,6 @@ <regex>(drop|reject|accept)</regex> </constraint> </properties> + <defaultValue>drop</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/firewall/name-default-log.xml.i b/interface-definitions/include/firewall/enable-default-log.xml.i index 1d0ff9497..1e64edc6e 100644 --- a/interface-definitions/include/firewall/name-default-log.xml.i +++ b/interface-definitions/include/firewall/enable-default-log.xml.i @@ -1,4 +1,4 @@ -<!-- include start from firewall/name-default-log.xml.i --> +<!-- include start from firewall/enable-default-log.xml.i --> <leafNode name="enable-default-log"> <properties> <help>Option to log packets hitting default-action</help> diff --git a/interface-definitions/include/inbound-interface.xml.i b/interface-definitions/include/inbound-interface.xml.i new file mode 100644 index 000000000..3289bbf8f --- /dev/null +++ b/interface-definitions/include/inbound-interface.xml.i @@ -0,0 +1,11 @@ +<!-- include start from inbound-interface.xml.i --> +<leafNode name="inbound-interface"> + <properties> + <help>Inbound interface of NAT traffic</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/ipv4-address-prefix.xml.i b/interface-definitions/include/ipv4-address-prefix.xml.i new file mode 100644 index 000000000..f5be6f1fe --- /dev/null +++ b/interface-definitions/include/ipv4-address-prefix.xml.i @@ -0,0 +1,19 @@ +<!-- include start from ipv4-address-prefix.xml.i --> +<leafNode name="address"> + <properties> + <help>IP address, prefix</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/nat.xml.in b/interface-definitions/nat.xml.in index 9295b631f..501ff05d3 100644 --- a/interface-definitions/nat.xml.in +++ b/interface-definitions/nat.xml.in @@ -14,15 +14,7 @@ #include <include/nat-rule.xml.i> <tagNode name="rule"> <children> - <leafNode name="inbound-interface"> - <properties> - <help>Inbound interface of NAT traffic</help> - <completionHelp> - <list>any</list> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/inbound-interface.xml.i> <node name="translation"> <properties> <help>Inside NAT IP (destination NAT only)</help> @@ -65,6 +57,17 @@ <children> #include <include/nat-rule.xml.i> <tagNode name="rule"> + <properties> + <help>Rule number for NAT</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of NAT rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>NAT rule number must be between 1 and 999999</constraintErrorMessage> + </properties> <children> #include <include/nat-interface.xml.i> <node name="translation"> @@ -110,6 +113,38 @@ </tagNode> </children> </node> + <node name="static"> + <properties> + <help>Static NAT (one-to-one)</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Rule number for NAT</help> + </properties> + <children> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>NAT destination parameters</help> + </properties> + <children> + #include <include/ipv4-address-prefix.xml.i> + </children> + </node> + #include <include/inbound-interface.xml.i> + <node name="translation"> + <properties> + <help>Translation address or prefix</help> + </properties> + <children> + #include <include/ipv4-address-prefix.xml.i> + </children> + </node> + </children> + </tagNode> + </children> + </node> </children> </node> </interfaceDefinition> diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in index a10c9b08f..c2a9a8d94 100644 --- a/interface-definitions/policy-route.xml.in +++ b/interface-definitions/policy-route.xml.in @@ -12,7 +12,7 @@ </properties> <children> #include <include/generic-description.xml.i> - #include <include/firewall/name-default-log.xml.i> + #include <include/firewall/enable-default-log.xml.i> <tagNode name="rule"> <properties> <help>Policy rule number</help> @@ -61,7 +61,7 @@ </properties> <children> #include <include/generic-description.xml.i> - #include <include/firewall/name-default-log.xml.i> + #include <include/firewall/enable-default-log.xml.i> <tagNode name="rule"> <properties> <help>Policy rule number</help> diff --git a/interface-definitions/protocols-rpki.xml.in b/interface-definitions/protocols-rpki.xml.in index 68762ff9a..4535d3990 100644 --- a/interface-definitions/protocols-rpki.xml.in +++ b/interface-definitions/protocols-rpki.xml.in @@ -12,15 +12,15 @@ <help>RPKI cache server address</help> <valueHelp> <format>ipv4</format> - <description>IP address of NTP server</description> + <description>IP address of RPKI server</description> </valueHelp> <valueHelp> <format>ipv6</format> - <description>IPv6 address of NTP server</description> + <description>IPv6 address of RPKI server</description> </valueHelp> <valueHelp> <format>hostname</format> - <description>Fully qualified domain name of NTP server</description> + <description>Fully qualified domain name of RPKI server</description> </valueHelp> <constraint> <validator name="ipv4-address"/> diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in index dca4c59d1..dc3408c3d 100644 --- a/interface-definitions/zone-policy.xml.in +++ b/interface-definitions/zone-policy.xml.in @@ -19,7 +19,7 @@ </properties> <children> #include <include/generic-description.xml.i> - #include <include/firewall/name-default-log.xml.i> + #include <include/firewall/enable-default-log.xml.i> <leafNode name="default-action"> <properties> <help>Default-action for traffic coming into this zone</help> diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in index 5dea5b91b..c8ca117be 100644 --- a/op-mode-definitions/dns-forwarding.xml.in +++ b/op-mode-definitions/dns-forwarding.xml.in @@ -19,26 +19,6 @@ </node> </children> </node> - <node name="dns"> - <properties> - <help>Show DNS information</help> - </properties> - <children> - <node name="forwarding"> - <properties> - <help>Show DNS forwarding information</help> - </properties> - <children> - <leafNode name="statistics"> - <properties> - <help>Show DNS forwarding statistics</help> - </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_statistics.py</command> - </leafNode> - </children> - </node> - </children> - </node> </children> </node> <node name="show"> diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index 1280fc238..b8deb3311 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -236,7 +236,7 @@ class EthernetIf(Interface): enabled, fixed = self.ethtool.get_large_receive_offload() if enabled != state: if not fixed: - return self.set_interface('gro', 'on' if state else 'off') + return self.set_interface('lro', 'on' if state else 'off') else: print('Adapter does not support changing large-receive-offload settings!') return False @@ -273,7 +273,7 @@ class EthernetIf(Interface): enabled, fixed = self.ethtool.get_scatter_gather() if enabled != state: if not fixed: - return self.set_interface('gro', 'on' if state else 'off') + return self.set_interface('sg', 'on' if state else 'off') else: print('Adapter does not support changing scatter-gather settings!') return False @@ -293,7 +293,7 @@ class EthernetIf(Interface): enabled, fixed = self.ethtool.get_tcp_segmentation_offload() if enabled != state: if not fixed: - return self.set_interface('gro', 'on' if state else 'off') + return self.set_interface('tso', 'on' if state else 'off') else: print('Adapter does not support changing tcp-segmentation-offload settings!') return False @@ -359,5 +359,5 @@ class EthernetIf(Interface): for rx_tx, size in config['ring_buffer'].items(): self.set_ring_buffer(rx_tx, size) - # call base class first + # call base class last super().update(config) diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 628f7b3a2..7e3545c87 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -105,6 +105,8 @@ def run(module): subparser = subparsers.add_parser(function_name, help=functions[function_name].__doc__) type_hints = typing.get_type_hints(functions[function_name]) + if 'return' in type_hints: + del type_hints['return'] for opt in type_hints: th = type_hints[opt] diff --git a/python/vyos/template.py b/python/vyos/template.py index eb7f06480..9804308c1 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -550,7 +550,7 @@ def nft_rule(rule_conf, fw_name, rule_id, ip_name='ip'): @register_filter('nft_default_rule') def nft_default_rule(fw_conf, fw_name): output = ['counter'] - default_action = fw_conf.get('default_action', 'accept') + default_action = fw_conf['default_action'] if 'enable_default_log' in fw_conf: action_suffix = default_action[:1].upper() diff --git a/smoketest/configs/service-https b/smoketest/configs/service-https deleted file mode 100644 index d478d5731..000000000 --- a/smoketest/configs/service-https +++ /dev/null @@ -1,55 +0,0 @@ -interfaces { - ethernet eth0 { - address 192.168.150.1/24 - } -} -service { - https { - certificates { - system-generated-certificate { - lifetime 365 - } - } - } -} -system { - config-management { - commit-revisions 100 - } - console { - device ttyS0 { - speed 115200 - } - } - host-name vyos - login { - user vyos { - authentication { - encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ - plaintext-password "" - } - } - } - ntp { - server time1.vyos.net { - } - server time2.vyos.net { - } - server time3.vyos.net { - } - } - syslog { - global { - facility all { - level info - } - facility protocols { - level debug - } - } - } -} - -// Warning: Do not remove the following line. -// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@6:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrf@2:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" -// Release version: 1.4-rolling-202106290839 diff --git a/smoketest/configs/vpn-openconnect-sstp b/smoketest/configs/vpn-openconnect-sstp index 45e6dd9b2..59a26f501 100644 --- a/smoketest/configs/vpn-openconnect-sstp +++ b/smoketest/configs/vpn-openconnect-sstp @@ -75,6 +75,7 @@ vpn { subnet 192.168.170.0/24 } gateway-address 192.168.150.1 + port 8443 ssl { ca-cert-file /config/auth/ovpn_test_ca.pem cert-file /config/auth/ovpn_test_server.pem diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index 65b676451..fe2682d50 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -26,7 +26,7 @@ from vyos.util import process_named_running CONFIG_FILE = '/run/powerdns/recursor.conf' FORWARD_FILE = '/run/powerdns/recursor.forward-zones.conf' HOSTSD_FILE = '/run/powerdns/recursor.vyos-hostsd.conf.lua' -PROCESS_NAME= 'pdns-r/worker' +PROCESS_NAME= 'pdns_recursor' base_path = ['service', 'dns', 'forwarding'] diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 07eca722f..f0ea1a1e5 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -206,9 +206,31 @@ def get_config(config=None): firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + # We have gathered the dict representation of the CLI, but there are + # default options which we need to update into the dictionary retrived. + # XXX: T2665: we currently have no nice way for defaults under tag + # nodes, thus we load the defaults "by hand" default_values = defaults(base) + for tmp in ['name', 'ipv6_name']: + if tmp in default_values: + del default_values[tmp] + firewall = dict_merge(default_values, firewall) + # Merge in defaults for IPv4 ruleset + if 'name' in firewall: + default_values = defaults(base + ['name']) + for name in firewall['name']: + firewall['name'][name] = dict_merge(default_values, + firewall['name'][name]) + + # Merge in defaults for IPv6 ruleset + if 'ipv6_name' in firewall: + default_values = defaults(base + ['ipv6-name']) + for ipv6_name in firewall['ipv6_name']: + firewall['ipv6_name'][ipv6_name] = dict_merge(default_values, + firewall['ipv6_name'][ipv6_name]) + firewall['policy_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) firewall['interfaces'] = get_firewall_interfaces(conf) firewall['zone_policy'] = get_firewall_zones(conf) @@ -315,7 +337,7 @@ def verify_nested_group(group_name, group, groups, seen): if g in seen: raise ConfigError(f'Group "{group_name}" has a circular reference') - + seen.append(g) if 'include' in groups[g]: @@ -378,7 +400,7 @@ def cleanup_commands(firewall): if firewall['geoip_updated']: geoip_key = 'deleted_ipv6_name' if table == 'ip6 filter' else 'deleted_name' geoip_list = dict_search_args(firewall, 'geoip_updated', geoip_key) or [] - + json_str = cmd(f'nft -t -j list table {table}') obj = loads(json_str) @@ -420,7 +442,7 @@ def cleanup_commands(firewall): if set_name.startswith('GEOIP_CC_') and set_name in geoip_list: commands_sets.append(f'delete set {table} {set_name}') continue - + if set_name.startswith("RECENT_"): commands_sets.append(f'delete set {table} {set_name}') continue @@ -520,7 +542,7 @@ def apply(firewall): if install_result == 1: raise ConfigError('Failed to apply firewall') - # set fireall group domain-group xxx + # set firewall group domain-group xxx if 'group' in firewall: if 'domain_group' in firewall['group']: # T970 Enable a resolver (systemd daemon) that checks diff --git a/src/conf_mode/high-availability.py b/src/conf_mode/high-availability.py index e14050dd3..8a959dc79 100755 --- a/src/conf_mode/high-availability.py +++ b/src/conf_mode/high-availability.py @@ -88,15 +88,12 @@ def verify(ha): if not {'password', 'type'} <= set(group_config['authentication']): raise ConfigError(f'Authentication requires both type and passwortd to be set in VRRP group "{group}"') - # We can not use a VRID once per interface + # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction + # We also need to make sure VRID is not used twice on the same interface with the + # same address family. + interface = group_config['interface'] vrid = group_config['vrid'] - tmp = {'interface': interface, 'vrid': vrid} - if tmp in used_vrid_if: - raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface}"!') - used_vrid_if.append(tmp) - - # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction # XXX: filter on map object is destructive, so we force it to list. # Additionally, filter objects always evaluate to True, empty or not, @@ -109,6 +106,11 @@ def verify(ha): raise ConfigError(f'VRRP group "{group}" mixes IPv4 and IPv6 virtual addresses, this is not allowed.\n' \ 'Create individual groups for IPv4 and IPv6!') if vaddrs4: + tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv4'} + if tmp in used_vrid_if: + raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface} with address family IPv4"!') + used_vrid_if.append(tmp) + if 'hello_source_address' in group_config: if is_ipv6(group_config['hello_source_address']): raise ConfigError(f'VRRP group "{group}" uses IPv4 but hello-source-address is IPv6!') @@ -118,6 +120,11 @@ def verify(ha): raise ConfigError(f'VRRP group "{group}" uses IPv4 but peer-address is IPv6!') if vaddrs6: + tmp = {'interface': interface, 'vrid': vrid, 'ipver': 'IPv6'} + if tmp in used_vrid_if: + raise ConfigError(f'VRID "{vrid}" can only be used once on interface "{interface} with address family IPv6"!') + used_vrid_if.append(tmp) + if 'hello_source_address' in group_config: if is_ipv4(group_config['hello_source_address']): raise ConfigError(f'VRRP group "{group}" uses IPv6 but hello-source-address is IPv4!') diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py index 20f2b1975..4c65bc0b6 100755 --- a/src/conf_mode/interfaces-pseudo-ethernet.py +++ b/src/conf_mode/interfaces-pseudo-ethernet.py @@ -15,11 +15,13 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. from sys import exit +from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed from vyos.configdict import is_source_interface +from vyos.configdict import leaf_node_changed from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -49,6 +51,9 @@ def get_config(config=None): mode = is_node_changed(conf, ['mode']) if mode: peth.update({'shutdown_required' : {}}) + if leaf_node_changed(conf, base + [ifname, 'mode']): + peth.update({'rebuild_required': {}}) + if 'source_interface' in peth: _, peth['parent'] = get_interface_dict(conf, ['interfaces', 'ethernet'], peth['source_interface']) @@ -77,21 +82,18 @@ def generate(peth): return None def apply(peth): - if 'deleted' in peth: - # delete interface - MACVLANIf(peth['ifname']).remove() - return None + # Check if the MACVLAN interface already exists + if 'rebuild_required' in peth or 'deleted' in peth: + if peth['ifname'] in interfaces(): + p = MACVLANIf(peth['ifname']) + # MACVLAN is always needs to be recreated, + # thus we can simply always delete it first. + p.remove() - # Check if MACVLAN interface already exists. Parameters like the underlaying - # source-interface device or mode can not be changed on the fly and the - # interface needs to be recreated from the bottom. - if 'mode_old' in peth: - MACVLANIf(peth['ifname']).remove() + if 'deleted' not in peth: + p = MACVLANIf(**peth) + p.update(peth) - # It is safe to "re-create" the interface always, there is a sanity check - # that the interface will only be create if its non existent - p = MACVLANIf(**peth) - p.update(peth) return None if __name__ == '__main__': diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 85819a77e..e75418ba5 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -44,7 +44,8 @@ if LooseVersion(kernel_version()) > LooseVersion('5.1'): else: k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] -nftables_nat_config = '/tmp/vyos-nat-rules.nft' +nftables_nat_config = '/run/nftables_nat.conf' +nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft' def get_handler(json, chain, target): """ Get nftable rule handler number of given chain/target combination. @@ -88,7 +89,7 @@ def get_config(config=None): # T2665: we must add the tagNode defaults individually until this is # moved to the base class - for direction in ['source', 'destination']: + for direction in ['source', 'destination', 'static']: if direction in nat: default_values = defaults(base + [direction, 'rule']) for rule in dict_search(f'{direction}.rule', nat) or []: @@ -178,24 +179,35 @@ def verify(nat): # common rule verification verify_rule(config, err_msg) + if dict_search('static.rule', nat): + for rule, config in dict_search('static.rule', nat).items(): + err_msg = f'Static NAT configuration error in rule {rule}:' + + if 'inbound_interface' not in config: + raise ConfigError(f'{err_msg}\n' \ + 'inbound-interface not specified') + + # common rule verification + verify_rule(config, err_msg) + return None def generate(nat): render(nftables_nat_config, 'firewall/nftables-nat.j2', nat) + render(nftables_static_nat_conf, 'firewall/nftables-static-nat.j2', nat) # dry-run newly generated configuration tmp = run(f'nft -c -f {nftables_nat_config}') if tmp > 0: - if os.path.exists(nftables_nat_config): - os.unlink(nftables_nat_config) raise ConfigError('Configuration file errors encountered!') + tmp = run(f'nft -c -f {nftables_nat_config}') + return None def apply(nat): cmd(f'nft -f {nftables_nat_config}') - if os.path.isfile(nftables_nat_config): - os.unlink(nftables_nat_config) + cmd(f'nft -f {nftables_static_nat_conf}') return None diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index 86985d765..e922edc4e 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -16,6 +16,7 @@ import os import re +from pathlib import Path from vyos.config import Config from vyos.configdict import dict_merge @@ -68,18 +69,15 @@ def verify(console): # amount of connected devices. We will resolve the fixed device name # to its dynamic device file - and create a new dict entry for it. by_bus_device = f'{by_bus_dir}/{device}' - if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): - device = os.path.basename(os.readlink(by_bus_device)) - - # If the device name still starts with usbXXX no matching tty was found - # and it can not be used as a serial interface - if device.startswith('usb'): - raise ConfigError(f'Device {device} does not support beeing used as tty') + # If the device name still starts with usbXXX no matching tty was found + # and it can not be used as a serial interface + if not os.path.isdir(by_bus_dir) or not os.path.exists(by_bus_device): + raise ConfigError(f'Device {device} does not support beeing used as tty') return None def generate(console): - base_dir = '/etc/systemd/system' + base_dir = '/run/systemd/system' # Remove all serial-getty configuration files in advance for root, dirs, files in os.walk(base_dir): for basename in files: @@ -90,7 +88,8 @@ def generate(console): if not console or 'device' not in console: return None - for device, device_config in console['device'].items(): + # replace keys in the config for ttyUSB items to use them in `apply()` later + for device in console['device'].copy(): if device.startswith('usb'): # It is much easiert to work with the native ttyUSBn name when using # getty, but that name may change across reboots - depending on the @@ -98,9 +97,17 @@ def generate(console): # to its dynamic device file - and create a new dict entry for it. by_bus_device = f'{by_bus_dir}/{device}' if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device): - device = os.path.basename(os.readlink(by_bus_device)) + device_updated = os.path.basename(os.readlink(by_bus_device)) + + # replace keys in the config to use them in `apply()` later + console['device'][device_updated] = console['device'][device] + del console['device'][device] + else: + raise ConfigError(f'Device {device} does not support beeing used as tty') + for device, device_config in console['device'].items(): config_file = base_dir + f'/serial-getty@{device}.service' + Path(f'{base_dir}/getty.target.wants').mkdir(exist_ok=True) getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service' render(config_file, 'getty/serial-getty.service.j2', device_config) diff --git a/src/etc/opennhrp/opennhrp-script.py b/src/etc/opennhrp/opennhrp-script.py index a5293c97e..bf25a7331 100755 --- a/src/etc/opennhrp/opennhrp-script.py +++ b/src/etc/opennhrp/opennhrp-script.py @@ -81,7 +81,13 @@ def vici_ike_terminate(list_ikeid: list[str]) -> bool: session = vici.Session() for ikeid in list_ikeid: logger.info(f'Terminating IKE SA with id {ikeid}') - session.terminate({'ike-id': ikeid, 'timeout': '-1'}) + session_generator = session.terminate( + {'ike-id': ikeid, 'timeout': '-1'}) + # a dummy `for` loop is required because of requirements + # from vici. Without a full iteration on the output, the + # command to vici may not be executed completely + for _ in session_generator: + pass return True except Exception as err: logger.error(f'Failed to terminate SA for IKE ids {list_ikeid}: {err}') @@ -175,13 +181,18 @@ def vici_initiate(conn: str, child_sa: str, src_addr: str, f'src_addr: {src_addr}, dst_addr: {dest_addr}') try: session = vici.Session() - session.initiate({ + session_generator = session.initiate({ 'ike': conn, 'child': child_sa, 'timeout': '-1', 'my-host': src_addr, 'other-host': dest_addr }) + # a dummy `for` loop is required because of requirements + # from vici. Without a full iteration on the output, the + # command to vici may not be executed completely + for _ in session_generator: + pass return True except Exception as err: logger.error(f'Unable to initiate connection {err}') diff --git a/src/services/api/graphql/bindings.py b/src/services/api/graphql/bindings.py index 049d59de7..0b1260912 100644 --- a/src/services/api/graphql/bindings.py +++ b/src/services/api/graphql/bindings.py @@ -17,6 +17,7 @@ import vyos.defaults from . graphql.queries import query from . graphql.mutations import mutation from . graphql.directives import directives_dict +from . graphql.errors import op_mode_error from . utils.schema_from_op_mode import generate_op_mode_definitions from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers @@ -27,6 +28,6 @@ def generate_schema(): type_defs = load_schema_from_path(api_schema_dir) - schema = make_executable_schema(type_defs, query, mutation, snake_case_fallback_resolvers, directives=directives_dict) + schema = make_executable_schema(type_defs, query, op_mode_error, mutation, snake_case_fallback_resolvers, directives=directives_dict) return schema diff --git a/src/services/api/graphql/graphql/errors.py b/src/services/api/graphql/graphql/errors.py new file mode 100644 index 000000000..1066300e0 --- /dev/null +++ b/src/services/api/graphql/graphql/errors.py @@ -0,0 +1,8 @@ + +from ariadne import InterfaceType + +op_mode_error = InterfaceType("OpModeError") + +@op_mode_error.type_resolver +def resolve_op_mode_error(obj, *_): + return obj['name'] diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index c8ae0f516..1b77cff87 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -22,6 +22,8 @@ from makefun import with_signature from .. import state from .. import key_auth from api.graphql.session.session import Session +from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code +from vyos.opmode import Error as OpModeError mutation = ObjectType("Mutation") @@ -86,10 +88,19 @@ def make_mutation_resolver(mutation_name, class_name, session_func): "success": True, "data": data } + except OpModeError as e: + typename = type(e).__name__ + return { + "success": False, + "errore": ['op_mode_error'], + "op_mode_error": {"name": f"{typename}", + "message": op_mode_err_msg.get(typename, "Unknown"), + "vyos_code": op_mode_err_code.get(typename, 9999)} + } except Exception as error: return { "success": False, - "errors": [str(error)] + "errors": [repr(error)] } return func_impl diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 921a66274..8ae61b704 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -22,6 +22,8 @@ from makefun import with_signature from .. import state from .. import key_auth from api.graphql.session.session import Session +from api.graphql.session.errors.op_mode_errors import op_mode_err_msg, op_mode_err_code +from vyos.opmode import Error as OpModeError query = ObjectType("Query") @@ -86,10 +88,19 @@ def make_query_resolver(query_name, class_name, session_func): "success": True, "data": data } + except OpModeError as e: + typename = type(e).__name__ + return { + "success": False, + "errors": ['op_mode_error'], + "op_mode_error": {"name": f"{typename}", + "message": op_mode_err_msg.get(typename, "Unknown"), + "vyos_code": op_mode_err_code.get(typename, 9999)} + } except Exception as error: return { "success": False, - "errors": [str(error)] + "errors": [repr(error)] } return func_impl diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py new file mode 100644 index 000000000..7ba75455d --- /dev/null +++ b/src/services/api/graphql/session/errors/op_mode_errors.py @@ -0,0 +1,13 @@ + + +op_mode_err_msg = { + "UnconfiguredSubsystem": "subsystem is not configured or not running", + "DataUnavailable": "data currently unavailable", + "PermissionDenied": "client does not have permission" +} + +op_mode_err_code = { + "UnconfiguredSubsystem": 2000, + "DataUnavailable": 2001, + "PermissionDenied": 1003 +} diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index 23bc7154c..93e1c328e 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -22,6 +22,7 @@ from vyos.config import Config from vyos.configtree import ConfigTree from vyos.defaults import directories from vyos.template import render +from vyos.opmode import Error as OpModeError from api.graphql.utils.util import load_op_mode_as_module, split_compound_op_mode_name @@ -177,10 +178,10 @@ class Session: mod = load_op_mode_as_module(f'{scriptname}') func = getattr(mod, func_name) - if len(list(data)) > 0: + try: res = func(True, **data) - else: - res = func(True) + except OpModeError as e: + raise e return res @@ -199,9 +200,9 @@ class Session: mod = load_op_mode_as_module(f'{scriptname}') func = getattr(mod, func_name) - if len(list(data)) > 0: + try: res = func(**data) - else: - res = func() + except OpModeError as e: + raise e return res diff --git a/src/services/api/graphql/utils/schema_from_op_mode.py b/src/services/api/graphql/utils/schema_from_op_mode.py index f990aae52..379d15250 100755 --- a/src/services/api/graphql/utils/schema_from_op_mode.py +++ b/src/services/api/graphql/utils/schema_from_op_mode.py @@ -21,7 +21,7 @@ import os import json import typing -from inspect import signature, getmembers, isfunction +from inspect import signature, getmembers, isfunction, isclass, getmro from jinja2 import Template from vyos.defaults import directories @@ -35,6 +35,7 @@ SCHEMA_PATH = directories['api_schema'] DATA_DIR = directories['data'] op_mode_include_file = os.path.join(DATA_DIR, 'op-mode-standardized.json') +op_mode_error_schema = 'op_mode_error.graphql' schema_data: dict = {'schema_name': '', 'schema_fields': []} @@ -53,6 +54,7 @@ type {{ schema_name }} { type {{ schema_name }}Result { data: {{ schema_name }} + op_mode_error: OpModeError success: Boolean! errors: [String] } @@ -76,6 +78,7 @@ type {{ schema_name }} { type {{ schema_name }}Result { data: {{ schema_name }} + op_mode_error: OpModeError success: Boolean! errors: [String] } @@ -85,6 +88,21 @@ extend type Mutation { } """ +error_template = """ +interface OpModeError { + name: String! + message: String! + vyos_code: Int! +} +{% for name in error_names %} +type {{ name }} implements OpModeError { + name: String! + message: String! + vyos_code: Int! +} +{%- endfor %} +""" + def _snake_to_pascal_case(name: str) -> str: res = ''.join(map(str.title, name.split('_'))) return res @@ -136,7 +154,30 @@ def create_schema(func_name: str, base_name: str, func: callable) -> str: return res +def create_error_schema(): + from vyos import opmode + + e = Exception + err_types = getmembers(opmode, isclass) + err_types = [k for k in err_types if issubclass(k[1], e)] + # drop base class, to be replaced by interface type. Find the class + # programmatically, in case the base class name changes. + for i in range(len(err_types)): + if err_types[i][1] in getmro(err_types[i-1][1]): + del err_types[i] + break + err_names = [k[0] for k in err_types] + error_data = {'error_names': err_names} + j2_template = Template(error_template) + res = j2_template.render(error_data) + + return res + def generate_op_mode_definitions(): + out = create_error_schema() + with open(f'{SCHEMA_PATH}/{op_mode_error_schema}', 'w') as f: + f.write(out) + with open(op_mode_include_file) as f: op_mode_files = json.load(f) |