diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index bd9aa7bbf..933894447 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,57 +1,60 @@ <!-- All PR should follow this template to allow a clean and transparent review --> <!-- Text placed between these delimiters is considered a comment and is not rendered --> ## Change Summary <!--- Provide a general summary of your changes in the Title above --> ## Types of changes <!--- What types of changes does your code introduce? Put an 'x' in all the boxes that apply. NOTE: Markdown requires no leading or trailing whitespace inside the [ ] for checking the box, please use [x] --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Code style update (formatting, renaming) - [ ] Refactoring (no functional changes) - [ ] Migration from an old Vyatta component to vyos-1x, please link to related PR inside obsoleted component - [ ] Other (please describe): ## Related Task(s) <!-- All submitted PRs must be linked to a Task on Phabricator. --> * https://vyos.dev/Txxxx +## Related PR(s) +<!-- Link here any PRs in other repositories that are required by this PR --> + ## Component(s) name <!-- A rather incomplete list of components: ethernet, wireguard, bgp, mpls, ldp, l2tp, dhcp ... --> ## Proposed changes <!--- Describe your changes in detail --> ## How to test <!--- Please describe in detail how you tested your changes. Include details of your testing environment, and the tests you ran. When pasting configs, logs, shell output, backtraces, and other large chunks of text, surround this text with triple backtics ``` like this ``` --> ## Smoketest result <!-- Provide the output of the smoketest ``` $ /usr/libexec/vyos/tests/smoke/cli/test_xxx_feature.py test_01_simple_options (__main__.TestFeature.test_01_simple_options) ... ok ``` --> ## Checklist: <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!--- The entire development process is outlined here: https://docs.vyos.io/en/latest/contributing/development.html --> - [ ] I have read the [**CONTRIBUTING**](https://github.com/vyos/vyos-1x/blob/current/CONTRIBUTING.md) document - [ ] I have linked this PR to one or more Phabricator Task(s) - [ ] I have run the components [**SMOKETESTS**](https://github.com/vyos/vyos-1x/tree/current/smoketest/scripts/cli) if applicable - [ ] My commit headlines contain a valid Task id - [ ] My change requires a change to the documentation - [ ] I have updated the documentation accordingly diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 08732bd4c..a433c2522 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -1,35 +1,38 @@ { - "firewall": {"group_resync": ["conntrack", "nat", "policy-route"]}, + "firewall": {"conntrack": ["conntrack"], "group_resync": ["conntrack", "nat", "policy-route"]}, "http_api": {"https": ["https"]}, + "load_balancing_wan": {"conntrack": ["conntrack"]}, + "nat": {"conntrack": ["conntrack"]}, + "nat66": {"conntrack": ["conntrack"]}, "pki": { "ethernet": ["interfaces-ethernet"], "openvpn": ["interfaces-openvpn"], "https": ["https"], "ipsec": ["vpn_ipsec"], "openconnect": ["vpn_openconnect"], "sstp": ["vpn_sstp"] }, "qos": { "bonding": ["interfaces-bonding"], "bridge": ["interfaces-bridge"], "dummy": ["interfaces-dummy"], "ethernet": ["interfaces-ethernet"], "geneve": ["interfaces-geneve"], "input": ["interfaces-input"], "l2tpv3": ["interfaces-l2tpv3"], "loopback": ["interfaces-loopback"], "macsec": ["interfaces-macsec"], "openvpn": ["interfaces-openvpn"], "pppoe": ["interfaces-pppoe"], "pseudo-ethernet": ["interfaces-pseudo-ethernet"], "tunnel": ["interfaces-tunnel"], "vti": ["interfaces-vti"], "vxlan": ["interfaces-vxlan"], "wireguard": ["interfaces-wireguard"], "wireless": ["interfaces-wireless"], "wwan": ["interfaces-wwan"] }, "vpp": { "ethernet": ["interfaces-ethernet"] } } diff --git a/data/templates/conntrack/nftables-ct.j2 b/data/templates/conntrack/nftables-ct.j2 index 3a5b5a87c..895f61a55 100644 --- a/data/templates/conntrack/nftables-ct.j2 +++ b/data/templates/conntrack/nftables-ct.j2 @@ -1,58 +1,179 @@ #!/usr/sbin/nft -f {% import 'firewall/nftables-defines.j2' as group_tmpl %} -{% set nft_ct_ignore_name = 'VYOS_CT_IGNORE' %} -{% set nft_ct_timeout_name = 'VYOS_CT_TIMEOUT' %} - -# we first flush all chains and render the content from scratch - this makes -# any delta check obsolete -flush chain raw {{ nft_ct_ignore_name }} -flush chain raw {{ nft_ct_timeout_name }} - -table raw { - chain {{ nft_ct_ignore_name }} { +{% if first_install is not vyos_defined %} +delete table ip vyos_conntrack +{% endif %} +table ip vyos_conntrack { + chain VYOS_CT_IGNORE { {% if ignore.ipv4.rule is vyos_defined %} {% for rule, rule_config in ignore.ipv4.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} {{ rule_config | conntrack_ignore_rule(rule, ipv6=False) }} {% endfor %} {% endif %} return } - chain {{ nft_ct_timeout_name }} { + chain VYOS_CT_TIMEOUT { {% if timeout.custom.rule is vyos_defined %} {% for rule, rule_config in timeout.custom.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} {% endfor %} {% endif %} return } -{{ group_tmpl.groups(firewall_group, False, True) }} -} + chain PREROUTING { + type filter hook prerouting priority -300; policy accept; +{% if ipv4_firewall_action == 'accept' or ipv4_nat_action == 'accept' %} + counter jump VYOS_CT_HELPER +{% endif %} + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT + counter jump FW_CONNTRACK + counter jump NAT_CONNTRACK + counter jump WLB_CONNTRACK + notrack + } + + chain OUTPUT { + type filter hook output priority -300; policy accept; +{% if ipv4_firewall_action == 'accept' or ipv4_nat_action == 'accept' %} + counter jump VYOS_CT_HELPER +{% endif %} + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT + counter jump FW_CONNTRACK + counter jump NAT_CONNTRACK +{% if wlb_local_action %} + counter jump WLB_CONNTRACK +{% endif %} + notrack + } + + ct helper rpc_tcp { + type "rpc" protocol tcp; + } + + ct helper rpc_udp { + type "rpc" protocol udp; + } + + ct helper tns_tcp { + type "tns" protocol tcp; + } + + chain VYOS_CT_HELPER { +{% for module, module_conf in module_map.items() %} +{% if modules[module] is vyos_defined %} +{% if 'nftables' in module_conf %} +{% for rule in module_conf.nftables %} + {{ rule }} +{% endfor %} +{% endif %} +{% endif %} +{% endfor %} + return + } + + chain FW_CONNTRACK { + {{ ipv4_firewall_action }} + } + + chain NAT_CONNTRACK { + {{ ipv4_nat_action }} + } + + chain WLB_CONNTRACK { + {{ wlb_action }} + } -flush chain ip6 raw {{ nft_ct_ignore_name }} -flush chain ip6 raw {{ nft_ct_timeout_name }} +{% if firewall.group is vyos_defined %} +{{ group_tmpl.groups(firewall.group, False, True) }} +{% endif %} +} -table ip6 raw { - chain {{ nft_ct_ignore_name }} { +{% if first_install is not vyos_defined %} +delete table ip6 vyos_conntrack +{% endif %} +table ip6 vyos_conntrack { + chain VYOS_CT_IGNORE { {% if ignore.ipv6.rule is vyos_defined %} {% for rule, rule_config in ignore.ipv6.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} {{ rule_config | conntrack_ignore_rule(rule, ipv6=True) }} {% endfor %} {% endif %} return } - chain {{ nft_ct_timeout_name }} { + chain VYOS_CT_TIMEOUT { {% if timeout.custom.rule is vyos_defined %} {% for rule, rule_config in timeout.custom.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} {% endfor %} {% endif %} return } -{{ group_tmpl.groups(firewall_group, True, True) }} + chain PREROUTING { + type filter hook prerouting priority -300; policy accept; +{% if ipv6_firewall_action == 'accept' or ipv6_nat_action == 'accept' %} + counter jump VYOS_CT_HELPER +{% endif %} + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT + counter jump FW_CONNTRACK + counter jump NAT_CONNTRACK + notrack + } + + chain OUTPUT { + type filter hook output priority -300; policy accept; +{% if ipv6_firewall_action == 'accept' or ipv6_nat_action == 'accept' %} + counter jump VYOS_CT_HELPER +{% endif %} + counter jump VYOS_CT_IGNORE + counter jump VYOS_CT_TIMEOUT + counter jump FW_CONNTRACK + counter jump NAT_CONNTRACK + notrack + } + + ct helper rpc_tcp { + type "rpc" protocol tcp; + } + + ct helper rpc_udp { + type "rpc" protocol udp; + } + + ct helper tns_tcp { + type "tns" protocol tcp; + } + + chain VYOS_CT_HELPER { +{% for module, module_conf in module_map.items() %} +{% if modules[module] is vyos_defined %} +{% if 'nftables' in module_conf %} +{% for rule in module_conf.nftables %} + {{ rule }} +{% endfor %} +{% endif %} +{% endif %} +{% endfor %} + return + } + + chain FW_CONNTRACK { + {{ ipv6_firewall_action }} + } + + chain NAT_CONNTRACK { + {{ ipv6_nat_action }} + } + +{% if firewall.group is vyos_defined %} +{{ group_tmpl.groups(firewall.group, True, True) }} +{% endif %} } diff --git a/data/templates/firewall/nftables-nat.j2 b/data/templates/firewall/nftables-nat.j2 index dcf28da88..4254f6a0e 100644 --- a/data/templates/firewall/nftables-nat.j2 +++ b/data/templates/firewall/nftables-nat.j2 @@ -1,67 +1,46 @@ #!/usr/sbin/nft -f {% import 'firewall/nftables-defines.j2' as group_tmpl %} -{% if helper_functions is vyos_defined('remove') %} -{# NAT if going to be disabled - remove rules and targets from nftables #} -{% set base_command = 'delete rule ip raw' %} -{{ base_command }} PREROUTING handle {{ pre_ct_ignore }} -{{ base_command }} OUTPUT handle {{ out_ct_ignore }} -{{ base_command }} PREROUTING handle {{ pre_ct_conntrack }} -{{ base_command }} OUTPUT handle {{ out_ct_conntrack }} - -delete chain ip raw NAT_CONNTRACK - -{% elif helper_functions is vyos_defined('add') %} -{# NAT if enabled - add targets to nftables #} -add chain ip raw NAT_CONNTRACK -add rule ip raw NAT_CONNTRACK counter accept -{% set base_command = 'add rule ip raw' %} -{{ base_command }} PREROUTING position {{ pre_ct_ignore }} counter jump VYOS_CT_HELPER -{{ base_command }} OUTPUT position {{ out_ct_ignore }} counter jump VYOS_CT_HELPER -{{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK -{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK -{% endif %} - {% if first_install is not vyos_defined %} delete table ip vyos_nat {% endif %} {% if deleted is not vyos_defined %} table ip vyos_nat { # # Destination NAT rules build up here # chain PREROUTING { type nat hook prerouting priority -100; policy accept; counter jump VYOS_PRE_DNAT_HOOK {% if destination.rule is vyos_defined %} {% for rule, config in destination.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'destination') }} {% endfor %} {% endif %} } # # Source NAT rules build up here # chain POSTROUTING { type nat hook postrouting priority 100; policy accept; counter jump VYOS_PRE_SNAT_HOOK {% if source.rule is vyos_defined %} {% for rule, config in source.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'source') }} {% endfor %} {% endif %} } chain VYOS_PRE_DNAT_HOOK { return } chain VYOS_PRE_SNAT_HOOK { return } {{ group_tmpl.groups(firewall_group, False, True) }} } {% endif %} diff --git a/data/templates/firewall/nftables-nat66.j2 b/data/templates/firewall/nftables-nat66.j2 index 27b3eec88..67eb2c109 100644 --- a/data/templates/firewall/nftables-nat66.j2 +++ b/data/templates/firewall/nftables-nat66.j2 @@ -1,57 +1,40 @@ #!/usr/sbin/nft -f -{% if helper_functions is vyos_defined('remove') %} -{# NAT if going to be disabled - remove rules and targets from nftables #} -{% set base_command = 'delete rule ip6 raw' %} -{{ base_command }} PREROUTING handle {{ pre_ct_conntrack }} -{{ base_command }} OUTPUT handle {{ out_ct_conntrack }} - -delete chain ip6 raw NAT_CONNTRACK - -{% elif helper_functions is vyos_defined('add') %} -{# NAT if enabled - add targets to nftables #} -add chain ip6 raw NAT_CONNTRACK -add rule ip6 raw NAT_CONNTRACK counter accept -{% set base_command = 'add rule ip6 raw' %} -{{ base_command }} PREROUTING position {{ pre_ct_conntrack }} counter jump NAT_CONNTRACK -{{ base_command }} OUTPUT position {{ out_ct_conntrack }} counter jump NAT_CONNTRACK -{% endif %} - {% if first_install is not vyos_defined %} delete table ip6 vyos_nat {% endif %} table ip6 vyos_nat { # # Destination NAT66 rules build up here # chain PREROUTING { type nat hook prerouting priority -100; policy accept; counter jump VYOS_DNPT_HOOK {% if destination.rule is vyos_defined %} {% for rule, config in destination.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'destination', ipv6=True) }} {% endfor %} {% endif %} } # # Source NAT66 rules build up here # chain POSTROUTING { type nat hook postrouting priority 100; policy accept; counter jump VYOS_SNPT_HOOK {% if source.rule is vyos_defined %} {% for rule, config in source.rule.items() if config.disable is not vyos_defined %} {{ config | nat_rule(rule, 'source', ipv6=True) }} {% endfor %} {% endif %} } chain VYOS_DNPT_HOOK { return } chain VYOS_SNPT_HOOK { return } } diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index db010257d..1564b3ef8 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -1,295 +1,283 @@ #!/usr/sbin/nft -f {% import 'firewall/nftables-defines.j2' as group_tmpl %} {% import 'firewall/nftables-bridge.j2' as bridge_tmpl %} {% import 'firewall/nftables-offload.j2' as offload %} -flush chain raw FW_CONNTRACK -flush chain ip6 raw FW_CONNTRACK - flush chain raw vyos_global_rpfilter flush chain ip6 raw vyos_global_rpfilter table raw { - chain FW_CONNTRACK { - {{ ipv4_conntrack_action }} - } - chain vyos_global_rpfilter { {% if global_options.source_validation is vyos_defined('loose') %} fib saddr oif 0 counter drop {% elif global_options.source_validation is vyos_defined('strict') %} fib saddr . iif oif 0 counter drop {% endif %} return } } table ip6 raw { - chain FW_CONNTRACK { - {{ ipv6_conntrack_action }} - } - chain vyos_global_rpfilter { {% if global_options.ipv6_source_validation is vyos_defined('loose') %} fib saddr oif 0 counter drop {% elif global_options.ipv6_source_validation is vyos_defined('strict') %} fib saddr . iif oif 0 counter drop {% endif %} return } } {% if first_install is not vyos_defined %} delete table ip vyos_filter {% endif %} table ip vyos_filter { {% if ipv4 is vyos_defined %} {% set ns = namespace(sets=[]) %} {% if ipv4.forward is vyos_defined %} {% for prior, conf in ipv4.forward.items() %} {% set def_action = conf.default_action %} chain VYOS_FORWARD_{{ prior }} { type filter hook forward priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('FWD', prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} {% if ipv4.input is vyos_defined %} {% for prior, conf in ipv4.input.items() %} {% set def_action = conf.default_action %} chain VYOS_INPUT_{{ prior }} { type filter hook input priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('INP',prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} {% if ipv4.output is vyos_defined %} {% for prior, conf in ipv4.output.items() %} {% set def_action = conf.default_action %} chain VYOS_OUTPUT_{{ prior }} { type filter hook output priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('OUT', prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['OUT_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} chain VYOS_FRAG_MARK { type filter hook prerouting priority -450; policy accept; ip frag-off & 0x3fff != 0 meta mark set 0xffff1 return } {% if ipv4.prerouting is vyos_defined %} {% for prior, conf in ipv4.prerouting.items() %} {% set def_action = conf.default_action %} chain VYOS_PREROUTING_{{ prior }} { type filter hook prerouting priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('PRE', prior, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['PRE_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} {{ conf | nft_default_rule(prior) }} } {% endfor %} {% endif %} {% if ipv4.name is vyos_defined %} {% for name_text, conf in ipv4.name.items() %} chain NAME_{{ name_text }} { {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('NAM', name_text, rule_id) }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} {{ conf | nft_default_rule(name_text) }} } {% endfor %} {% endif %} {% for set_name in ns.sets %} set RECENT_{{ set_name }} { type ipv4_addr size 65535 flags dynamic } {% endfor %} {% for set_name in ip_fqdn %} set FQDN_{{ set_name }} { type ipv4_addr flags interval } {% endfor %} {% if geoip_updated.name is vyos_defined %} {% for setname in geoip_updated.name %} set {{ setname }} { type ipv4_addr flags interval } {% endfor %} {% endif %} {% endif %} {{ group_tmpl.groups(group, False, True) }} } {% if first_install is not vyos_defined %} delete table ip6 vyos_filter {% endif %} table ip6 vyos_filter { {% if ipv6 is vyos_defined %} {% set ns = namespace(sets=[]) %} {% if ipv6.forward is vyos_defined %} {% for prior, conf in ipv6.forward.items() %} {% set def_action = conf.default_action %} chain VYOS_IPV6_FORWARD_{{ prior }} { type filter hook forward priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('FWD', prior, rule_id ,'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['FWD_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} {% if ipv6.input is vyos_defined %} {% for prior, conf in ipv6.input.items() %} {% set def_action = conf.default_action %} chain VYOS_IPV6_INPUT_{{ prior }} { type filter hook input priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('INP', prior, rule_id ,'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['INP_' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} {% if ipv6.output is vyos_defined %} {% for prior, conf in ipv6.output.items() %} {% set def_action = conf.default_action %} chain VYOS_IPV6_OUTPUT_{{ prior }} { type filter hook output priority {{ prior }}; policy {{ def_action }}; {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('OUT', prior, rule_id ,'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['OUT_ ' + prior + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} } {% endfor %} {% endif %} chain VYOS_FRAG6_MARK { type filter hook prerouting priority -450; policy accept; exthdr frag exists meta mark set 0xffff1 return } {% if ipv6.name is vyos_defined %} {% for name_text, conf in ipv6.name.items() %} chain NAME6_{{ name_text }} { {% if conf.rule is vyos_defined %} {% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %} {{ rule_conf | nft_rule('NAM', name_text, rule_id, 'ip6') }} {% if rule_conf.recent is vyos_defined %} {% set ns.sets = ns.sets + ['NAM_' + name_text + '_' + rule_id] %} {% endif %} {% endfor %} {% endif %} {{ conf | nft_default_rule(name_text, ipv6=True) }} } {% endfor %} {% endif %} {% for set_name in ns.sets %} set RECENT6_{{ set_name }} { type ipv6_addr size 65535 flags dynamic } {% endfor %} {% for set_name in ip6_fqdn %} set FQDN_{{ set_name }} { type ipv6_addr flags interval } {% endfor %} {% if geoip_updated.ipv6_name is vyos_defined %} {% for setname in geoip_updated.ipv6_name %} set {{ setname }} { type ipv6_addr flags interval } {% endfor %} {% endif %} {% endif %} {{ group_tmpl.groups(group, True, True) }} } ## Bridge Firewall {% if first_install is not vyos_defined %} delete table bridge vyos_filter {% endif %} {% if bridge is vyos_defined %} table bridge vyos_filter { {{ bridge_tmpl.bridge(bridge) }} {{ group_tmpl.groups(group, False, False) }} } {% endif %} -table inet vyos_offload +{% if first_install is not vyos_defined %} delete table inet vyos_offload +{% endif %} table inet vyos_offload { -{% if flowtable_enabled %} -{% if global_options.flow_offload.hardware.interface is vyos_defined %} +{% if global_options.flow_offload.hardware.interface is vyos_defined %} {{- offload.render_flowtable('VYOS_FLOWTABLE_hardware', global_options.flow_offload.hardware.interface | list, priority='filter - 2', hardware_offload=true) }} chain VYOS_OFFLOAD_hardware { type filter hook forward priority filter - 2; policy accept; ct state { established, related } meta l4proto { tcp, udp } flow add @VYOS_FLOWTABLE_hardware } -{% endif %} -{% if global_options.flow_offload.software.interface is vyos_defined %} +{% endif %} +{% if global_options.flow_offload.software.interface is vyos_defined %} {{- offload.render_flowtable('VYOS_FLOWTABLE_software', global_options.flow_offload.software.interface | list, priority='filter - 1') }} chain VYOS_OFFLOAD_software { type filter hook forward priority filter - 1; policy accept; ct state { established, related } meta l4proto { tcp, udp } flow add @VYOS_FLOWTABLE_software } -{% endif %} {% endif %} } diff --git a/data/vyos-firewall-init.conf b/data/vyos-firewall-init.conf index 7e258e6f1..cd7d5011f 100644 --- a/data/vyos-firewall-init.conf +++ b/data/vyos-firewall-init.conf @@ -1,159 +1,56 @@ #!/usr/sbin/nft -f # Required by wanloadbalance table ip nat { chain VYOS_PRE_SNAT_HOOK { type nat hook postrouting priority 99; policy accept; return } } table inet mangle { + # Used by system flow-accounting chain FORWARD { type filter hook forward priority -150; policy accept; } } table raw { chain VYOS_TCP_MSS { type filter hook forward priority -300; policy accept; } chain vyos_global_rpfilter { return } chain vyos_rpfilter { type filter hook prerouting priority -300; policy accept; counter jump vyos_global_rpfilter } - chain PREROUTING { + # Used by system flow-accounting + chain VYOS_PREROUTING_HOOK { type filter hook prerouting priority -300; policy accept; - counter jump VYOS_CT_IGNORE - counter jump VYOS_CT_TIMEOUT - counter jump VYOS_CT_PREROUTING_HOOK - counter jump FW_CONNTRACK - notrack - } - - chain OUTPUT { - type filter hook output priority -300; policy accept; - counter jump VYOS_CT_IGNORE - counter jump VYOS_CT_TIMEOUT - counter jump VYOS_CT_OUTPUT_HOOK - counter jump FW_CONNTRACK - notrack - } - - ct helper rpc_tcp { - type "rpc" protocol tcp; - } - - ct helper rpc_udp { - type "rpc" protocol udp; - } - - ct helper tns_tcp { - type "tns" protocol tcp; - } - - chain VYOS_CT_HELPER { - ct helper set "rpc_tcp" tcp dport {111} return - ct helper set "rpc_udp" udp dport {111} return - ct helper set "tns_tcp" tcp dport {1521,1525,1536} return - return - } - - chain VYOS_CT_IGNORE { - return - } - - chain VYOS_CT_TIMEOUT { - return - } - - chain VYOS_CT_PREROUTING_HOOK { - return - } - - chain VYOS_CT_OUTPUT_HOOK { - return - } - - chain FW_CONNTRACK { - return } } table ip6 raw { chain VYOS_TCP_MSS { type filter hook forward priority -300; policy accept; } chain vyos_global_rpfilter { return } chain vyos_rpfilter { type filter hook prerouting priority -300; policy accept; counter jump vyos_global_rpfilter } - chain PREROUTING { + # Used by system flow-accounting + chain VYOS_PREROUTING_HOOK { type filter hook prerouting priority -300; policy accept; - counter jump VYOS_CT_IGNORE - counter jump VYOS_CT_TIMEOUT - counter jump VYOS_CT_PREROUTING_HOOK - counter jump FW_CONNTRACK - notrack - } - - chain OUTPUT { - type filter hook output priority -300; policy accept; - counter jump VYOS_CT_IGNORE - counter jump VYOS_CT_TIMEOUT - counter jump VYOS_CT_OUTPUT_HOOK - counter jump FW_CONNTRACK - notrack - } - - ct helper rpc_tcp { - type "rpc" protocol tcp; - } - - ct helper rpc_udp { - type "rpc" protocol udp; - } - - ct helper tns_tcp { - type "tns" protocol tcp; - } - - chain VYOS_CT_HELPER { - ct helper set "rpc_tcp" tcp dport {111} return - ct helper set "rpc_udp" udp dport {111} return - ct helper set "tns_tcp" tcp dport {1521,1525,1536} return - return - } - - chain VYOS_CT_IGNORE { - return - } - - chain VYOS_CT_TIMEOUT { - return - } - - chain VYOS_CT_PREROUTING_HOOK { - return - } - - chain VYOS_CT_OUTPUT_HOOK { - return - } - - chain FW_CONNTRACK { - return } } diff --git a/interface-definitions/include/firewall/common-rule-inet.xml.i b/interface-definitions/include/firewall/common-rule-inet.xml.i index 7a2eb86d4..e51dd0056 100644 --- a/interface-definitions/include/firewall/common-rule-inet.xml.i +++ b/interface-definitions/include/firewall/common-rule-inet.xml.i @@ -1,374 +1,375 @@ <!-- include start from firewall/common-rule-inet.xml.i --> #include <include/firewall/action.xml.i> #include <include/generic-description.xml.i> #include <include/firewall/dscp.xml.i> #include <include/firewall/packet-options.xml.i> #include <include/firewall/connection-mark.xml.i> #include <include/firewall/nft-queue.xml.i> <leafNode name="disable"> <properties> <help>Option to disable firewall rule</help> <valueless/> </properties> </leafNode> <node name="fragment"> <properties> <help>IP fragment match</help> </properties> <children> <leafNode name="match-frag"> <properties> <help>Second and further fragments of fragmented packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-non-frag"> <properties> <help>Head fragments or unfragmented packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="ipsec"> <properties> <help>Inbound IPsec packets</help> </properties> <children> <leafNode name="match-ipsec"> <properties> <help>Inbound IPsec packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-none"> <properties> <help>Inbound non-IPsec packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="limit"> <properties> <help>Rate limit using a token bucket filter</help> </properties> <children> <leafNode name="burst"> <properties> <help>Maximum number of packets to allow in excess of rate</help> <valueHelp> <format>u32:0-4294967295</format> <description>Maximum number of packets to allow in excess of rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> <leafNode name="rate"> <properties> <help>Maximum average matching rate</help> <valueHelp> <format>txt</format> <description>integer/unit (Example: 5/minute)</description> </valueHelp> <constraint> <regex>\d+/(second|minute|hour|day)</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="log"> <properties> <help>Option to log packets matching rule</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable log</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable log</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="log"> <properties> <help>Option to log packets matching rule</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable log</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable log</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> #include <include/firewall/rule-log-options.xml.i> <node name="connection-status"> <properties> <help>Connection status</help> </properties> <children> <leafNode name="nat"> <properties> <help>NAT connection status</help> <completionHelp> <list>destination source</list> </completionHelp> <valueHelp> <format>destination</format> <description>Match connections that are subject to destination NAT</description> </valueHelp> <valueHelp> <format>source</format> <description>Match connections that are subject to source NAT</description> </valueHelp> <constraint> <regex>(destination|source)</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> <script>${vyos_completion_dir}/list_protocols.sh</script> <list>all tcp_udp</list> </completionHelp> <valueHelp> <format>all</format> <description>All IP protocols</description> </valueHelp> <valueHelp> <format>tcp_udp</format> <description>Both TCP and UDP</description> </valueHelp> <valueHelp> <format>u32:0-255</format> <description>IP protocol number</description> </valueHelp> <valueHelp> <format><protocol></format> <description>IP protocol name</description> </valueHelp> <valueHelp> <format>!<protocol></format> <description>IP protocol name</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> </leafNode> <node name="recent"> <properties> <help>Parameters for matching recently seen sources</help> </properties> <children> <leafNode name="count"> <properties> <help>Source addresses seen more than N times</help> <valueHelp> <format>u32:1-255</format> <description>Source addresses seen more than N times</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-255"/> </constraint> </properties> </leafNode> <leafNode name="time"> <properties> <help>Source addresses seen in the last second/minute/hour</help> <completionHelp> <list>second minute hour</list> </completionHelp> <valueHelp> <format>second</format> <description>Source addresses seen COUNT times in the last second</description> </valueHelp> <valueHelp> <format>minute</format> <description>Source addresses seen COUNT times in the last minute</description> </valueHelp> <valueHelp> <format>hour</format> <description>Source addresses seen COUNT times in the last hour</description> </valueHelp> <constraint> <regex>(second|minute|hour)</regex> </constraint> </properties> </leafNode> </children> </node> <node name="state"> <properties> <help>Session state</help> </properties> <children> <leafNode name="established"> <properties> <help>Established state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="invalid"> <properties> <help>Invalid state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="new"> <properties> <help>New state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="related"> <properties> <help>Related state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> </children> </node> #include <include/firewall/tcp-flags.xml.i> +#include <include/firewall/tcp-mss.xml.i> <node name="time"> <properties> <help>Time to match rule</help> </properties> <children> <leafNode name="startdate"> <properties> <help>Date to start matching rule</help> <valueHelp> <format>txt</format> <description>Enter date using following notation - YYYY-MM-DD</description> </valueHelp> <constraint> <regex>(\d{4}\-\d{2}\-\d{2})</regex> </constraint> </properties> </leafNode> <leafNode name="starttime"> <properties> <help>Time of day to start matching rule</help> <valueHelp> <format>txt</format> <description>Enter time using using 24 hour notation - hh:mm:ss</description> </valueHelp> <constraint> <regex>([0-2][0-9](\:[0-5][0-9]){1,2})</regex> </constraint> </properties> </leafNode> <leafNode name="stopdate"> <properties> <help>Date to stop matching rule</help> <valueHelp> <format>txt</format> <description>Enter date using following notation - YYYY-MM-DD</description> </valueHelp> <constraint> <regex>(\d{4}\-\d{2}\-\d{2})</regex> </constraint> </properties> </leafNode> <leafNode name="stoptime"> <properties> <help>Time of day to stop matching rule</help> <valueHelp> <format>txt</format> <description>Enter time using using 24 hour notation - hh:mm:ss</description> </valueHelp> <constraint> <regex>([0-2][0-9](\:[0-5][0-9]){1,2})</regex> </constraint> </properties> </leafNode> <leafNode name="weekdays"> <properties> <help>Comma separated weekdays to match rule on</help> <valueHelp> <format>txt</format> <description>Name of day (Monday, Tuesday, Wednesday, Thursdays, Friday, Saturday, Sunday)</description> </valueHelp> <valueHelp> <format>u32:0-6</format> <description>Day number (0 = Sunday ... 6 = Saturday)</description> </valueHelp> </properties> </leafNode> </children> </node> <!-- include end --> \ No newline at end of file diff --git a/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i b/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i index a1071a09a..e040c9b13 100644 --- a/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i +++ b/interface-definitions/include/firewall/common-rule-ipv4-raw.xml.i @@ -1,331 +1,332 @@ <!-- include start from firewall/common-rule-ipv4-raw.xml.i --> #include <include/firewall/action-and-notrack.xml.i> #include <include/generic-description.xml.i> #include <include/firewall/dscp.xml.i> #include <include/firewall/ttl.xml.i> #include <include/firewall/nft-queue.xml.i> <node name="destination"> <properties> <help>Destination parameters</help> </properties> <children> #include <include/firewall/address.xml.i> #include <include/firewall/address-mask.xml.i> #include <include/firewall/fqdn.xml.i> #include <include/firewall/geoip.xml.i> #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group.xml.i> </children> </node> <leafNode name="disable"> <properties> <help>Option to disable firewall rule</help> <valueless/> </properties> </leafNode> <node name="fragment"> <properties> <help>IP fragment match</help> </properties> <children> <leafNode name="match-frag"> <properties> <help>Second and further fragments of fragmented packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-non-frag"> <properties> <help>Head fragments or unfragmented packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="icmp"> <properties> <help>ICMP type and code information</help> </properties> <children> <leafNode name="code"> <properties> <help>ICMP code</help> <valueHelp> <format>u32:0-255</format> <description>ICMP code (0-255)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-255"/> </constraint> </properties> </leafNode> <leafNode name="type"> <properties> <help>ICMP type</help> <valueHelp> <format>u32:0-255</format> <description>ICMP type (0-255)</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-255"/> </constraint> </properties> </leafNode> #include <include/firewall/icmp-type-name.xml.i> </children> </node> <node name="ipsec"> <properties> <help>Inbound IPsec packets</help> </properties> <children> <leafNode name="match-ipsec"> <properties> <help>Inbound IPsec packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-none"> <properties> <help>Inbound non-IPsec packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="limit"> <properties> <help>Rate limit using a token bucket filter</help> </properties> <children> <leafNode name="burst"> <properties> <help>Maximum number of packets to allow in excess of rate</help> <valueHelp> <format>u32:0-4294967295</format> <description>Maximum number of packets to allow in excess of rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> <leafNode name="rate"> <properties> <help>Maximum average matching rate</help> <valueHelp> <format>txt</format> <description>integer/unit (Example: 5/minute)</description> </valueHelp> <constraint> <regex>\d+/(second|minute|hour|day)</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="log"> <properties> <help>Option to log packets matching rule</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable log</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable log</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> #include <include/firewall/rule-log-options.xml.i> <node name="connection-status"> <properties> <help>Connection status</help> </properties> <children> <leafNode name="nat"> <properties> <help>NAT connection status</help> <completionHelp> <list>destination source</list> </completionHelp> <valueHelp> <format>destination</format> <description>Match connections that are subject to destination NAT</description> </valueHelp> <valueHelp> <format>source</format> <description>Match connections that are subject to source NAT</description> </valueHelp> <constraint> <regex>(destination|source)</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> <script>${vyos_completion_dir}/list_protocols.sh</script> <list>all tcp_udp</list> </completionHelp> <valueHelp> <format>all</format> <description>All IP protocols</description> </valueHelp> <valueHelp> <format>tcp_udp</format> <description>Both TCP and UDP</description> </valueHelp> <valueHelp> <format>u32:0-255</format> <description>IP protocol number</description> </valueHelp> <valueHelp> <format><protocol></format> <description>IP protocol name</description> </valueHelp> <valueHelp> <format>!<protocol></format> <description>IP protocol name</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> </leafNode> <node name="recent"> <properties> <help>Parameters for matching recently seen sources</help> </properties> <children> <leafNode name="count"> <properties> <help>Source addresses seen more than N times</help> <valueHelp> <format>u32:1-255</format> <description>Source addresses seen more than N times</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-255"/> </constraint> </properties> </leafNode> <leafNode name="time"> <properties> <help>Source addresses seen in the last second/minute/hour</help> <completionHelp> <list>second minute hour</list> </completionHelp> <valueHelp> <format>second</format> <description>Source addresses seen COUNT times in the last second</description> </valueHelp> <valueHelp> <format>minute</format> <description>Source addresses seen COUNT times in the last minute</description> </valueHelp> <valueHelp> <format>hour</format> <description>Source addresses seen COUNT times in the last hour</description> </valueHelp> <constraint> <regex>(second|minute|hour)</regex> </constraint> </properties> </leafNode> </children> </node> <node name="source"> <properties> <help>Source parameters</help> </properties> <children> #include <include/firewall/address.xml.i> #include <include/firewall/address-mask.xml.i> #include <include/firewall/fqdn.xml.i> #include <include/firewall/geoip.xml.i> #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> #include <include/firewall/source-destination-group.xml.i> </children> </node> #include <include/firewall/tcp-flags.xml.i> +#include <include/firewall/tcp-mss.xml.i> <node name="time"> <properties> <help>Time to match rule</help> </properties> <children> <leafNode name="startdate"> <properties> <help>Date to start matching rule</help> <valueHelp> <format>txt</format> <description>Enter date using following notation - YYYY-MM-DD</description> </valueHelp> <constraint> <regex>(\d{4}\-\d{2}\-\d{2})</regex> </constraint> </properties> </leafNode> <leafNode name="starttime"> <properties> <help>Time of day to start matching rule</help> <valueHelp> <format>txt</format> <description>Enter time using using 24 hour notation - hh:mm:ss</description> </valueHelp> <constraint> <regex>([0-2][0-9](\:[0-5][0-9]){1,2})</regex> </constraint> </properties> </leafNode> <leafNode name="stopdate"> <properties> <help>Date to stop matching rule</help> <valueHelp> <format>txt</format> <description>Enter date using following notation - YYYY-MM-DD</description> </valueHelp> <constraint> <regex>(\d{4}\-\d{2}\-\d{2})</regex> </constraint> </properties> </leafNode> <leafNode name="stoptime"> <properties> <help>Time of day to stop matching rule</help> <valueHelp> <format>txt</format> <description>Enter time using using 24 hour notation - hh:mm:ss</description> </valueHelp> <constraint> <regex>([0-2][0-9](\:[0-5][0-9]){1,2})</regex> </constraint> </properties> </leafNode> <leafNode name="weekdays"> <properties> <help>Comma separated weekdays to match rule on</help> <valueHelp> <format>txt</format> <description>Name of day (Monday, Tuesday, Wednesday, Thursdays, Friday, Saturday, Sunday)</description> </valueHelp> <valueHelp> <format>u32:0-6</format> <description>Day number (0 = Sunday ... 6 = Saturday)</description> </valueHelp> </properties> </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/firewall/common-rule.xml.i b/interface-definitions/include/firewall/common-rule.xml.i index 7417a3c58..c62bf2c5f 100644 --- a/interface-definitions/include/firewall/common-rule.xml.i +++ b/interface-definitions/include/firewall/common-rule.xml.i @@ -1,386 +1,387 @@ <!-- include start from firewall/common-rule.xml.i --> #include <include/firewall/action.xml.i> #include <include/generic-description.xml.i> <node name="destination"> <properties> <help>Destination parameters</help> </properties> <children> #include <include/firewall/mac-address.xml.i> </children> </node> <leafNode name="disable"> <properties> <help>Option to disable firewall rule</help> <valueless/> </properties> </leafNode> <node name="fragment"> <properties> <help>IP fragment match</help> </properties> <children> <leafNode name="match-frag"> <properties> <help>Second and further fragments of fragmented packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-non-frag"> <properties> <help>Head fragments or unfragmented packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="inbound-interface"> <properties> <help>Match inbound-interface</help> </properties> <children> #include <include/firewall/match-interface.xml.i> </children> </node> <node name="outbound-interface"> <properties> <help>Match outbound-interface</help> </properties> <children> #include <include/firewall/match-interface.xml.i> </children> </node> <node name="ipsec"> <properties> <help>Inbound IPsec packets</help> </properties> <children> <leafNode name="match-ipsec"> <properties> <help>Inbound IPsec packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-none"> <properties> <help>Inbound non-IPsec packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="limit"> <properties> <help>Rate limit using a token bucket filter</help> </properties> <children> <leafNode name="burst"> <properties> <help>Maximum number of packets to allow in excess of rate</help> <valueHelp> <format>u32:0-4294967295</format> <description>Maximum number of packets to allow in excess of rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> <leafNode name="rate"> <properties> <help>Maximum average matching rate</help> <valueHelp> <format>txt</format> <description>integer/unit (Example: 5/minute)</description> </valueHelp> <constraint> <regex>\d+/(second|minute|hour|day)</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="log"> <properties> <help>Option to log packets matching rule</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable log</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable log</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> #include <include/firewall/rule-log-options.xml.i> <node name="connection-status"> <properties> <help>Connection status</help> </properties> <children> <leafNode name="nat"> <properties> <help>NAT connection status</help> <completionHelp> <list>destination source</list> </completionHelp> <valueHelp> <format>destination</format> <description>Match connections that are subject to destination NAT</description> </valueHelp> <valueHelp> <format>source</format> <description>Match connections that are subject to source NAT</description> </valueHelp> <constraint> <regex>^(destination|source)$</regex> </constraint> </properties> </leafNode> </children> </node> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> <script>${vyos_completion_dir}/list_protocols.sh</script> <list>all tcp_udp</list> </completionHelp> <valueHelp> <format>all</format> <description>All IP protocols</description> </valueHelp> <valueHelp> <format>tcp_udp</format> <description>Both TCP and UDP</description> </valueHelp> <valueHelp> <format>u32:0-255</format> <description>IP protocol number</description> </valueHelp> <valueHelp> <format><protocol></format> <description>IP protocol name</description> </valueHelp> <valueHelp> <format>!<protocol></format> <description>IP protocol name</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> </leafNode> <node name="recent"> <properties> <help>Parameters for matching recently seen sources</help> </properties> <children> <leafNode name="count"> <properties> <help>Source addresses seen more than N times</help> <valueHelp> <format>u32:1-255</format> <description>Source addresses seen more than N times</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-255"/> </constraint> </properties> </leafNode> <leafNode name="time"> <properties> <help>Source addresses seen in the last second/minute/hour</help> <completionHelp> <list>second minute hour</list> </completionHelp> <valueHelp> <format>second</format> <description>Source addresses seen COUNT times in the last second</description> </valueHelp> <valueHelp> <format>minute</format> <description>Source addresses seen COUNT times in the last minute</description> </valueHelp> <valueHelp> <format>hour</format> <description>Source addresses seen COUNT times in the last hour</description> </valueHelp> <constraint> <regex>(second|minute|hour)</regex> </constraint> </properties> </leafNode> </children> </node> <node name="source"> <properties> <help>Source parameters</help> </properties> <children> #include <include/firewall/address.xml.i> #include <include/firewall/source-destination-group.xml.i> #include <include/firewall/mac-address.xml.i> #include <include/firewall/port.xml.i> </children> </node> <node name="state"> <properties> <help>Session state</help> </properties> <children> <leafNode name="established"> <properties> <help>Established state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="invalid"> <properties> <help>Invalid state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="new"> <properties> <help>New state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="related"> <properties> <help>Related state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> </children> </node> #include <include/firewall/tcp-flags.xml.i> +#include <include/firewall/tcp-mss.xml.i> <node name="time"> <properties> <help>Time to match rule</help> </properties> <children> <leafNode name="startdate"> <properties> <help>Date to start matching rule</help> <valueHelp> <format>txt</format> <description>Enter date using following notation - YYYY-MM-DD</description> </valueHelp> <constraint> <regex>(\d{4}\-\d{2}\-\d{2})</regex> </constraint> </properties> </leafNode> <leafNode name="starttime"> <properties> <help>Time of day to start matching rule</help> <valueHelp> <format>txt</format> <description>Enter time using using 24 hour notation - hh:mm:ss</description> </valueHelp> <constraint> <regex>([0-2][0-9](\:[0-5][0-9]){1,2})</regex> </constraint> </properties> </leafNode> <leafNode name="stopdate"> <properties> <help>Date to stop matching rule</help> <valueHelp> <format>txt</format> <description>Enter date using following notation - YYYY-MM-DD</description> </valueHelp> <constraint> <regex>(\d{4}\-\d{2}\-\d{2})</regex> </constraint> </properties> </leafNode> <leafNode name="stoptime"> <properties> <help>Time of day to stop matching rule</help> <valueHelp> <format>txt</format> <description>Enter time using using 24 hour notation - hh:mm:ss</description> </valueHelp> <constraint> <regex>([0-2][0-9](\:[0-5][0-9]){1,2})</regex> </constraint> </properties> </leafNode> <leafNode name="weekdays"> <properties> <help>Comma separated weekdays to match rule on</help> <valueHelp> <format>txt</format> <description>Name of day (Monday, Tuesday, Wednesday, Thursdays, Friday, Saturday, Sunday)</description> </valueHelp> <valueHelp> <format>u32:0-6</format> <description>Day number (0 = Sunday ... 6 = Saturday)</description> </valueHelp> </properties> </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/firewall/tcp-flags.xml.i b/interface-definitions/include/firewall/tcp-flags.xml.i index e2ce7b9fd..36546c2e4 100644 --- a/interface-definitions/include/firewall/tcp-flags.xml.i +++ b/interface-definitions/include/firewall/tcp-flags.xml.i @@ -1,135 +1,119 @@ <!-- include start from firewall/tcp-flags.xml.i --> <node name="tcp"> <properties> - <help>TCP flags to match</help> + <help>TCP options to match</help> </properties> <children> <node name="flags"> <properties> <help>TCP flags to match</help> </properties> <children> <leafNode name="syn"> <properties> <help>Synchronise flag</help> <valueless/> </properties> </leafNode> <leafNode name="ack"> <properties> <help>Acknowledge flag</help> <valueless/> </properties> </leafNode> <leafNode name="fin"> <properties> <help>Finish flag</help> <valueless/> </properties> </leafNode> <leafNode name="rst"> <properties> <help>Reset flag</help> <valueless/> </properties> </leafNode> <leafNode name="urg"> <properties> <help>Urgent flag</help> <valueless/> </properties> </leafNode> <leafNode name="psh"> <properties> <help>Push flag</help> <valueless/> </properties> </leafNode> <leafNode name="ecn"> <properties> <help>Explicit Congestion Notification flag</help> <valueless/> </properties> </leafNode> <leafNode name="cwr"> <properties> <help>Congestion Window Reduced flag</help> <valueless/> </properties> </leafNode> <node name="not"> <properties> <help>Match flags not set</help> </properties> <children> <leafNode name="syn"> <properties> <help>Synchronise flag</help> <valueless/> </properties> </leafNode> <leafNode name="ack"> <properties> <help>Acknowledge flag</help> <valueless/> </properties> </leafNode> <leafNode name="fin"> <properties> <help>Finish flag</help> <valueless/> </properties> </leafNode> <leafNode name="rst"> <properties> <help>Reset flag</help> <valueless/> </properties> </leafNode> <leafNode name="urg"> <properties> <help>Urgent flag</help> <valueless/> </properties> </leafNode> <leafNode name="psh"> <properties> <help>Push flag</help> <valueless/> </properties> </leafNode> <leafNode name="ecn"> <properties> <help>Explicit Congestion Notification flag</help> <valueless/> </properties> </leafNode> <leafNode name="cwr"> <properties> <help>Congestion Window Reduced flag</help> <valueless/> </properties> </leafNode> </children> </node> </children> </node> - <leafNode name="mss"> - <properties> - <help>Maximum segment size (MSS)</help> - <valueHelp> - <format>u32:1-16384</format> - <description>Maximum segment size</description> - </valueHelp> - <valueHelp> - <format><min>-<max></format> - <description>TCP MSS range (use '-' as delimiter)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--allow-range --range 1-16384"/> - </constraint> - </properties> - </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/firewall/tcp-mss.xml.i b/interface-definitions/include/firewall/tcp-mss.xml.i new file mode 100644 index 000000000..dc49b4272 --- /dev/null +++ b/interface-definitions/include/firewall/tcp-mss.xml.i @@ -0,0 +1,25 @@ +<!-- include start from firewall/tcp-mss.xml.i --> +<node name="tcp"> + <properties> + <help>TCP options to match</help> + </properties> + <children> + <leafNode name="mss"> + <properties> + <help>Maximum segment size (MSS)</help> + <valueHelp> + <format>u32:1-16384</format> + <description>Maximum segment size</description> + </valueHelp> + <valueHelp> + <format><min>-<max></format> + <description>TCP MSS range (use '-' as delimiter)</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--allow-range --range 1-16384"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/policy/route-common.xml.i b/interface-definitions/include/policy/route-common.xml.i index 216ec9bea..6551d23ab 100644 --- a/interface-definitions/include/policy/route-common.xml.i +++ b/interface-definitions/include/policy/route-common.xml.i @@ -1,360 +1,361 @@ <!-- include start from policy/route-common.xml.i --> #include <include/policy/route-rule-action.xml.i> #include <include/generic-description.xml.i> <leafNode name="disable"> <properties> <help>Option to disable firewall rule</help> <valueless/> </properties> </leafNode> <node name="fragment"> <properties> <help>IP fragment match</help> </properties> <children> <leafNode name="match-frag"> <properties> <help>Second and further fragments of fragmented packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-non-frag"> <properties> <help>Head fragments or unfragmented packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="ipsec"> <properties> <help>Inbound IPsec packets</help> </properties> <children> <leafNode name="match-ipsec"> <properties> <help>Inbound IPsec packets</help> <valueless/> </properties> </leafNode> <leafNode name="match-none"> <properties> <help>Inbound non-IPsec packets</help> <valueless/> </properties> </leafNode> </children> </node> <node name="limit"> <properties> <help>Rate limit using a token bucket filter</help> </properties> <children> <leafNode name="burst"> <properties> <help>Maximum number of packets to allow in excess of rate</help> <valueHelp> <format>u32:0-4294967295</format> <description>Maximum number of packets to allow in excess of rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> <leafNode name="rate"> <properties> <help>Maximum average matching rate</help> <valueHelp> <format>u32:0-4294967295</format> <description>Maximum average matching rate</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> </children> </node> <leafNode name="log"> <properties> <help>Option to log packets matching rule</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable log</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable log</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> <script>cat /etc/protocols | sed -e '/^#.*/d' | awk '{ print $1 }'</script> </completionHelp> <valueHelp> <format>all</format> <description>All IP protocols</description> </valueHelp> <valueHelp> <format>tcp_udp</format> <description>Both TCP and UDP</description> </valueHelp> <valueHelp> <format>0-255</format> <description>IP protocol number</description> </valueHelp> <valueHelp> <format>!<protocol></format> <description>IP protocol number</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> <defaultValue>all</defaultValue> </leafNode> <node name="recent"> <properties> <help>Parameters for matching recently seen sources</help> </properties> <children> <leafNode name="count"> <properties> <help>Source addresses seen more than N times</help> <valueHelp> <format>u32:1-255</format> <description>Source addresses seen more than N times</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-255"/> </constraint> </properties> </leafNode> <leafNode name="time"> <properties> <help>Source addresses seen in the last N seconds</help> <valueHelp> <format>u32:0-4294967295</format> <description>Source addresses seen in the last N seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-4294967295"/> </constraint> </properties> </leafNode> </children> </node> <node name="set"> <properties> <help>Packet modifications</help> </properties> <children> <leafNode name="connection-mark"> <properties> <help>Connection marking</help> <valueHelp> <format>u32:0-2147483647</format> <description>Connection marking</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-2147483647"/> </constraint> </properties> </leafNode> <leafNode name="dscp"> <properties> <help>Packet Differentiated Services Codepoint (DSCP)</help> <valueHelp> <format>u32:0-63</format> <description>DSCP number</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 0-63"/> </constraint> </properties> </leafNode> <leafNode name="mark"> <properties> <help>Packet marking</help> <valueHelp> <format>u32:1-2147483647</format> <description>Packet marking</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-2147483647"/> </constraint> </properties> </leafNode> <leafNode name="table"> <properties> <help>Routing table to forward packet with</help> <valueHelp> <format>u32:1-200</format> <description>Table number</description> </valueHelp> <valueHelp> <format>main</format> <description>Main table</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-200"/> <regex>(main)</regex> </constraint> <completionHelp> <list>main</list> <path>protocols static table</path> </completionHelp> </properties> </leafNode> <leafNode name="tcp-mss"> <properties> <help>TCP Maximum Segment Size</help> <valueHelp> <format>u32:500-1460</format> <description>Explicitly set TCP MSS value</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 500-1460"/> </constraint> </properties> </leafNode> </children> </node> <node name="state"> <properties> <help>Session state</help> </properties> <children> <leafNode name="established"> <properties> <help>Established state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="invalid"> <properties> <help>Invalid state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="new"> <properties> <help>New state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> <leafNode name="related"> <properties> <help>Related state</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Enable</description> </valueHelp> <valueHelp> <format>disable</format> <description>Disable</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> </leafNode> </children> </node> #include <include/firewall/tcp-flags.xml.i> +#include <include/firewall/tcp-mss.xml.i> <node name="time"> <properties> <help>Time to match rule</help> </properties> <children> <leafNode name="monthdays"> <properties> <help>Monthdays to match rule on</help> </properties> </leafNode> <leafNode name="startdate"> <properties> <help>Date to start matching rule</help> </properties> </leafNode> <leafNode name="starttime"> <properties> <help>Time of day to start matching rule</help> </properties> </leafNode> <leafNode name="stopdate"> <properties> <help>Date to stop matching rule</help> </properties> </leafNode> <leafNode name="stoptime"> <properties> <help>Time of day to stop matching rule</help> </properties> </leafNode> <leafNode name="utc"> <properties> <help>Interpret times for startdate, stopdate, starttime and stoptime to be UTC</help> <valueless/> </properties> </leafNode> <leafNode name="weekdays"> <properties> <help>Weekdays to match rule on</help> </properties> </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/system-conntrack.xml.in b/interface-definitions/system-conntrack.xml.in index 78d19090c..4452f1a74 100644 --- a/interface-definitions/system-conntrack.xml.in +++ b/interface-definitions/system-conntrack.xml.in @@ -1,447 +1,449 @@ <?xml version="1.0"?> <interfaceDefinition> <node name="system"> <children> <node name="conntrack" owner="${vyos_conf_scripts_dir}/conntrack.py"> <properties> <help>Connection Tracking Engine Options</help> <!-- Before NAT and conntrack-sync are configured --> <priority>218</priority> </properties> <children> <leafNode name="flow-accounting"> <properties> <help>Enable connection tracking flow accounting</help> <valueless/> </properties> </leafNode> <leafNode name="expect-table-size"> <properties> <help>Size of connection tracking expect table</help> <valueHelp> <format>u32:1-50000000</format> <description>Number of entries allowed in connection tracking expect table</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-50000000"/> </constraint> </properties> <defaultValue>2048</defaultValue> </leafNode> <leafNode name="hash-size"> <properties> <help>Hash size for connection tracking table</help> <valueHelp> <format>u32:1-50000000</format> <description>Size of hash to use for connection tracking table</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-50000000"/> </constraint> </properties> <defaultValue>32768</defaultValue> </leafNode> <node name="ignore"> <properties> <help>Customized rules to ignore selective connection tracking</help> </properties> <children> <node name="ipv4"> <properties> <help>IPv4 rules</help> </properties> <children> <tagNode name="rule"> <properties> <help>Rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number of conntrack ignore rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/generic-description.xml.i> <node name="destination"> <properties> <help>Destination parameters</help> </properties> <children> #include <include/firewall/source-destination-group-ipv4.xml.i> #include <include/nat-address.xml.i> #include <include/nat-port.xml.i> </children> </node> <leafNode name="inbound-interface"> <properties> <help>Interface to ignore connections tracking on</help> <completionHelp> <list>any</list> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> </leafNode> #include <include/ip-protocol.xml.i> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> <script>${vyos_completion_dir}/list_protocols.sh</script> <list>all tcp_udp</list> </completionHelp> <valueHelp> <format>all</format> <description>All IP protocols</description> </valueHelp> <valueHelp> <format>tcp_udp</format> <description>Both TCP and UDP</description> </valueHelp> <valueHelp> <format>u32:0-255</format> <description>IP protocol number</description> </valueHelp> <valueHelp> <format><protocol></format> <description>IP protocol name</description> </valueHelp> <valueHelp> <format>!<protocol></format> <description>IP protocol name</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> </leafNode> <node name="source"> <properties> <help>Source parameters</help> </properties> <children> #include <include/firewall/source-destination-group-ipv4.xml.i> #include <include/nat-address.xml.i> #include <include/nat-port.xml.i> </children> </node> + #include <include/firewall/tcp-flags.xml.i> </children> </tagNode> </children> </node> <node name="ipv6"> <properties> <help>IPv6 rules</help> </properties> <children> <tagNode name="rule"> <properties> <help>Rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number of conntrack ignore rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/generic-description.xml.i> <node name="destination"> <properties> <help>Destination parameters</help> </properties> <children> #include <include/firewall/address-ipv6.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/nat-port.xml.i> </children> </node> <leafNode name="inbound-interface"> <properties> <help>Interface to ignore connections tracking on</help> <completionHelp> <list>any</list> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> </leafNode> #include <include/ip-protocol.xml.i> <leafNode name="protocol"> <properties> <help>Protocol to match (protocol name, number, or "all")</help> <completionHelp> <script>${vyos_completion_dir}/list_protocols.sh</script> <list>all tcp_udp</list> </completionHelp> <valueHelp> <format>all</format> <description>All IP protocols</description> </valueHelp> <valueHelp> <format>tcp_udp</format> <description>Both TCP and UDP</description> </valueHelp> <valueHelp> <format>u32:0-255</format> <description>IP protocol number</description> </valueHelp> <valueHelp> <format><protocol></format> <description>IP protocol name</description> </valueHelp> <valueHelp> <format>!<protocol></format> <description>IP protocol name</description> </valueHelp> <constraint> <validator name="ip-protocol"/> </constraint> </properties> </leafNode> <node name="source"> <properties> <help>Source parameters</help> </properties> <children> #include <include/firewall/address-ipv6.xml.i> #include <include/firewall/source-destination-group-ipv6.xml.i> #include <include/nat-port.xml.i> </children> </node> + #include <include/firewall/tcp-flags.xml.i> </children> </tagNode> </children> </node> </children> </node> <node name="log"> <properties> <help>Log connection tracking events per protocol</help> </properties> <children> <node name="icmp"> <properties> <help>Log connection tracking events for ICMP</help> </properties> <children> #include <include/conntrack/log-common.xml.i> </children> </node> <node name="other"> <properties> <help>Log connection tracking events for all protocols other than TCP, UDP and ICMP</help> </properties> <children> #include <include/conntrack/log-common.xml.i> </children> </node> <node name="tcp"> <properties> <help>Log connection tracking events for TCP</help> </properties> <children> #include <include/conntrack/log-common.xml.i> </children> </node> <node name="udp"> <properties> <help>Log connection tracking events for UDP</help> </properties> <children> #include <include/conntrack/log-common.xml.i> </children> </node> </children> </node> <node name="modules"> <properties> <help>Connection tracking modules</help> </properties> <children> <leafNode name="ftp"> <properties> <help>FTP connection tracking</help> <valueless/> </properties> </leafNode> <leafNode name="h323"> <properties> <help>H.323 connection tracking</help> <valueless/> </properties> </leafNode> <leafNode name="nfs"> <properties> <help>NFS connection tracking</help> <valueless/> </properties> </leafNode> <leafNode name="pptp"> <properties> <help>PPTP connection tracking</help> <valueless/> </properties> </leafNode> <leafNode name="sip"> <properties> <help>SIP connection tracking</help> <valueless/> </properties> </leafNode> <leafNode name="sqlnet"> <properties> <help>SQLnet connection tracking</help> <valueless/> </properties> </leafNode> <leafNode name="tftp"> <properties> <help>TFTP connection tracking</help> <valueless/> </properties> </leafNode> </children> </node> <leafNode name="table-size"> <properties> <help>Size of connection tracking table</help> <valueHelp> <format>u32:1-50000000</format> <description>Number of entries allowed in connection tracking table</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-50000000"/> </constraint> </properties> <defaultValue>262144</defaultValue> </leafNode> <node name="tcp"> <properties> <help>TCP options</help> </properties> <children> <leafNode name="half-open-connections"> <properties> <help>Maximum number of TCP half-open connections</help> <valueHelp> <format>u32:1-2147483647</format> <description>Generic connection timeout in seconds</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-2147483647"/> </constraint> </properties> <defaultValue>512</defaultValue> </leafNode> <leafNode name="loose"> <properties> <help>Policy to track previously established connections</help> <completionHelp> <list>enable disable</list> </completionHelp> <valueHelp> <format>enable</format> <description>Allow tracking of previously established connections</description> </valueHelp> <valueHelp> <format>disable</format> <description>Do not allow tracking of previously established connections</description> </valueHelp> <constraint> <regex>(enable|disable)</regex> </constraint> </properties> <defaultValue>enable</defaultValue> </leafNode> <leafNode name="max-retrans"> <properties> <help>Maximum number of packets that can be retransmitted without received an ACK</help> <valueHelp> <format>u32:1-255</format> <description>Number of packets to be retransmitted</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-255"/> </constraint> </properties> <defaultValue>3</defaultValue> </leafNode> </children> </node> <node name="timeout"> <properties> <help>Connection timeout options</help> </properties> <children> <node name="custom"> <properties> <help>Define custom timeouts per connection</help> </properties> <children> <tagNode name="rule"> <properties> <help>Rule number</help> <valueHelp> <format>u32:1-999999</format> <description>Number of conntrack rule</description> </valueHelp> <constraint> <validator name="numeric" argument="--range 1-999999"/> </constraint> <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> #include <include/generic-description.xml.i> <node name="destination"> <properties> <help>Destination parameters</help> </properties> <children> #include <include/nat-address.xml.i> #include <include/nat-port.xml.i> </children> </node> <leafNode name="inbound-interface"> <properties> <help>Interface to ignore connections tracking on</help> <completionHelp> <list>any</list> <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> </leafNode> #include <include/ip-protocol.xml.i> <node name="protocol"> <properties> <help>Customize protocol specific timers, one protocol configuration per rule</help> </properties> <children> #include <include/conntrack/timeout-common-protocols.xml.i> </children> </node> <node name="source"> <properties> <help>Source parameters</help> </properties> <children> #include <include/nat-address.xml.i> #include <include/nat-port.xml.i> </children> </node> </children> </tagNode> </children> </node> #include <include/conntrack/timeout-common-protocols.xml.i> </children> </node> </children> </node> </children> </node> </interfaceDefinition> diff --git a/python/vyos/template.py b/python/vyos/template.py index add4d3ce5..3be486cc4 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -1,821 +1,826 @@ # Copyright 2019-2023 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 # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library 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 # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import functools import os from jinja2 import Environment from jinja2 import FileSystemLoader from jinja2 import ChainableUndefined from vyos.defaults import directories from vyos.utils.dict import dict_search_args from vyos.utils.file import makedir from vyos.utils.permission import chmod from vyos.utils.permission import chown # Holds template filters registered via register_filter() _FILTERS = {} _TESTS = {} # reuse Environments with identical settings to improve performance @functools.lru_cache(maxsize=2) def _get_environment(location=None): if location is None: loc_loader=FileSystemLoader(directories["templates"]) else: loc_loader=FileSystemLoader(location) env = Environment( # Don't check if template files were modified upon re-rendering auto_reload=False, # Cache up to this number of templates for quick re-rendering cache_size=100, loader=loc_loader, trim_blocks=True, undefined=ChainableUndefined, extensions=['jinja2.ext.loopcontrols'] ) env.filters.update(_FILTERS) env.tests.update(_TESTS) return env def register_filter(name, func=None): """Register a function to be available as filter in templates under given name. It can also be used as a decorator, see below in this module for examples. :raise RuntimeError: when trying to register a filter after a template has been rendered already :raise ValueError: when trying to register a name which was taken already """ if func is None: return functools.partial(register_filter, name) if _get_environment.cache_info().currsize: raise RuntimeError( "Filters can only be registered before rendering the first template" ) if name in _FILTERS: raise ValueError(f"A filter with name {name!r} was registered already") _FILTERS[name] = func return func def register_test(name, func=None): """Register a function to be available as test in templates under given name. It can also be used as a decorator, see below in this module for examples. :raise RuntimeError: when trying to register a test after a template has been rendered already :raise ValueError: when trying to register a name which was taken already """ if func is None: return functools.partial(register_test, name) if _get_environment.cache_info().currsize: raise RuntimeError( "Tests can only be registered before rendering the first template" ) if name in _TESTS: raise ValueError(f"A test with name {name!r} was registered already") _TESTS[name] = func return func def render_to_string(template, content, formater=None, location=None): """Render a template from the template directory, raise on any errors. :param template: the path to the template relative to the template folder :param content: the dictionary of variables to put into rendering context :param formater: if given, it has to be a callable the rendered string is passed through The parsed template files are cached, so rendering the same file multiple times does not cause as too much overhead. If used everywhere, it could be changed to load the template from Python environment variables from an importable Python module generated when the Debian package is build (recovering the load time and overhead caused by having the file out of the code). """ template = _get_environment(location).get_template(template) rendered = template.render(content) if formater is not None: rendered = formater(rendered) return rendered def render( destination, template, content, formater=None, permission=None, user=None, group=None, location=None, ): """Render a template from the template directory to a file, raise on any errors. :param destination: path to the file to save the rendered template in :param permission: permission bitmask to set for the output file :param user: user to own the output file :param group: group to own the output file All other parameters are as for :func:`render_to_string`. """ # Create the directory if it does not exist folder = os.path.dirname(destination) makedir(folder, user, group) # As we are opening the file with 'w', we are performing the rendering before # calling open() to not accidentally erase the file if rendering fails rendered = render_to_string(template, content, formater, location) # Write to file with open(destination, "w") as file: chmod(file.fileno(), permission) chown(file.fileno(), user, group) file.write(rendered) ################################## # Custom template filters follow # ################################## @register_filter('force_to_list') def force_to_list(value): """ Convert scalars to single-item lists and leave lists untouched """ if isinstance(value, list): return value else: return [value] @register_filter('seconds_to_human') def seconds_to_human(seconds, separator=""): """ Convert seconds to human-readable values like 1d6h15m23s """ from vyos.utils.convert import seconds_to_human return seconds_to_human(seconds, separator=separator) @register_filter('bytes_to_human') def bytes_to_human(bytes, initial_exponent=0, precision=2): """ Convert bytes to human-readable values like 1.44M """ from vyos.utils.convert import bytes_to_human return bytes_to_human(bytes, initial_exponent=initial_exponent, precision=precision) @register_filter('human_to_bytes') def human_to_bytes(value): """ Convert a data amount with a unit suffix to bytes, like 2K to 2048 """ from vyos.utils.convert import human_to_bytes return human_to_bytes(value) @register_filter('ip_from_cidr') def ip_from_cidr(prefix): """ Take an IPv4/IPv6 CIDR host and strip cidr mask. Example: 192.0.2.1/24 -> 192.0.2.1, 2001:db8::1/64 -> 2001:db8::1 """ from ipaddress import ip_interface return str(ip_interface(prefix).ip) @register_filter('address_from_cidr') def address_from_cidr(prefix): """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". Example: 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: """ from ipaddress import ip_network return str(ip_network(prefix).network_address) @register_filter('bracketize_ipv6') def bracketize_ipv6(address): """ Place a passed IPv6 address into [] brackets, do nothing for IPv4 """ if is_ipv6(address): return f'[{address}]' return address @register_filter('dot_colon_to_dash') def dot_colon_to_dash(text): """ Replace dot and colon to dash for string Example: 192.0.2.1 => 192-0-2-1, 2001:db8::1 => 2001-db8--1 """ text = text.replace(":", "-") text = text.replace(".", "-") return text @register_filter('generate_uuid4') def generate_uuid4(text): """ Generate random unique ID Example: % uuid4() UUID('958ddf6a-ef14-4e81-8cfb-afb12456d1c5') """ from uuid import uuid4 return uuid4() @register_filter('netmask_from_cidr') def netmask_from_cidr(prefix): """ Take CIDR prefix and convert the prefix length to a "subnet mask". Example: - 192.0.2.0/24 -> 255.255.255.0 - 2001:db8::/48 -> ffff:ffff:ffff:: """ from ipaddress import ip_network return str(ip_network(prefix).netmask) @register_filter('netmask_from_ipv4') def netmask_from_ipv4(address): """ Take IP address and search all attached interface IP addresses for the given one. After address has been found, return the associated netmask. Example: - 172.18.201.10 -> 255.255.255.128 """ from netifaces import interfaces from netifaces import ifaddresses from netifaces import AF_INET for interface in interfaces(): tmp = ifaddresses(interface) if AF_INET in tmp: for af_addr in tmp[AF_INET]: if 'addr' in af_addr: if af_addr['addr'] == address: return af_addr['netmask'] raise ValueError @register_filter('is_ip_network') def is_ip_network(addr): """ Take IP(v4/v6) address and validate if the passed argument is a network or a host address. Example: - 192.0.2.0 -> False - 192.0.2.10/24 -> False - 192.0.2.0/24 -> True - 2001:db8:: -> False - 2001:db8::100 -> False - 2001:db8::/48 -> True - 2001:db8:1000::/64 -> True """ try: from ipaddress import ip_network # input variables must contain a / to indicate its CIDR notation if len(addr.split('/')) != 2: raise ValueError() ip_network(addr) return True except: return False @register_filter('network_from_ipv4') def network_from_ipv4(address): """ Take IP address and search all attached interface IP addresses for the given one. After address has been found, return the associated network address. Example: - 172.18.201.10 has mask 255.255.255.128 -> network is 172.18.201.0 """ netmask = netmask_from_ipv4(address) from ipaddress import ip_interface cidr_prefix = ip_interface(f'{address}/{netmask}').network return address_from_cidr(cidr_prefix) @register_filter('is_interface') def is_interface(interface): """ Check if parameter is a valid local interface name """ return os.path.exists(f'/sys/class/net/{interface}') @register_filter('is_ip') def is_ip(addr): """ Check addr if it is an IPv4 or IPv6 address """ return is_ipv4(addr) or is_ipv6(addr) @register_filter('is_ipv4') def is_ipv4(text): """ Filter IP address, return True on IPv4 address, False otherwise """ from ipaddress import ip_interface try: return ip_interface(text).version == 4 except: return False @register_filter('is_ipv6') def is_ipv6(text): """ Filter IP address, return True on IPv6 address, False otherwise """ from ipaddress import ip_interface try: return ip_interface(text).version == 6 except: return False @register_filter('first_host_address') def first_host_address(text): """ Return first usable (host) IP address from given prefix. Example: - 10.0.0.0/24 -> 10.0.0.1 - 2001:db8::/64 -> 2001:db8:: """ from ipaddress import ip_interface from ipaddress import IPv4Network from ipaddress import IPv6Network addr = ip_interface(text) if addr.version == 4: return str(addr.ip +1) return str(addr.ip) @register_filter('last_host_address') def last_host_address(text): """ Return first usable IP address from given prefix. Example: - 10.0.0.0/24 -> 10.0.0.254 - 2001:db8::/64 -> 2001:db8::ffff:ffff:ffff:ffff """ from ipaddress import ip_interface from ipaddress import IPv4Network from ipaddress import IPv6Network addr = ip_interface(text) if addr.version == 4: return str(IPv4Network(addr).broadcast_address - 1) return str(IPv6Network(addr).broadcast_address) @register_filter('inc_ip') def inc_ip(address, increment): """ Increment given IP address by 'increment' Example (inc by 2): - 10.0.0.0/24 -> 10.0.0.2 - 2001:db8::/64 -> 2001:db8::2 """ from ipaddress import ip_interface return str(ip_interface(address).ip + int(increment)) @register_filter('dec_ip') def dec_ip(address, decrement): """ Decrement given IP address by 'decrement' Example (inc by 2): - 10.0.0.0/24 -> 10.0.0.2 - 2001:db8::/64 -> 2001:db8::2 """ from ipaddress import ip_interface return str(ip_interface(address).ip - int(decrement)) @register_filter('compare_netmask') def compare_netmask(netmask1, netmask2): """ Compare two IP netmask if they have the exact same size. compare_netmask('10.0.0.0/8', '20.0.0.0/8') -> True compare_netmask('10.0.0.0/8', '20.0.0.0/16') -> False """ from ipaddress import ip_network try: return ip_network(netmask1).netmask == ip_network(netmask2).netmask except: return False @register_filter('isc_static_route') def isc_static_route(subnet, router): # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server # Option format is: # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3> # where bytes with the value 0 are omitted. from ipaddress import ip_network net = ip_network(subnet) # add netmask string = str(net.prefixlen) + ',' # add network bytes if net.prefixlen: width = net.prefixlen // 8 if net.prefixlen % 8: width += 1 string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ',' # add router bytes string += ','.join(router.split('.')) return string @register_filter('is_file') def is_file(filename): if os.path.exists(filename): return os.path.isfile(filename) return False @register_filter('get_dhcp_router') def get_dhcp_router(interface): """ Static routes can point to a router received by a DHCP reply. This helper is used to get the current default router from the DHCP reply. Returns False of no router is found, returns the IP address as string if a router is found. """ lease_file = directories['isc_dhclient_dir'] + f'/dhclient_{interface}.leases' if not os.path.exists(lease_file): return None from vyos.utils.file import read_file for line in read_file(lease_file).splitlines(): if 'option routers' in line: (_, _, address) = line.split() return address.rstrip(';') @register_filter('natural_sort') def natural_sort(iterable): import re from jinja2.runtime import Undefined if isinstance(iterable, Undefined) or iterable is None: return list() def convert(text): return int(text) if text.isdigit() else text.lower() def alphanum_key(key): return [convert(c) for c in re.split('([0-9]+)', str(key))] return sorted(iterable, key=alphanum_key) @register_filter('get_ipv4') def get_ipv4(interface): """ Get interface IPv4 addresses""" from vyos.ifconfig import Interface return Interface(interface).get_addr_v4() @register_filter('get_ipv6') def get_ipv6(interface): """ Get interface IPv6 addresses""" from vyos.ifconfig import Interface return Interface(interface).get_addr_v6() @register_filter('get_ip') def get_ip(interface): """ Get interface IP addresses""" from vyos.ifconfig import Interface return Interface(interface).get_addr() def get_first_ike_dh_group(ike_group): if ike_group and 'proposal' in ike_group: for priority, proposal in ike_group['proposal'].items(): if 'dh_group' in proposal: return 'dh-group' + proposal['dh_group'] return 'dh-group2' # Fallback on dh-group2 @register_filter('get_esp_ike_cipher') def get_esp_ike_cipher(group_config, ike_group=None): pfs_lut = { 'dh-group1' : 'modp768', 'dh-group2' : 'modp1024', 'dh-group5' : 'modp1536', 'dh-group14' : 'modp2048', 'dh-group15' : 'modp3072', 'dh-group16' : 'modp4096', 'dh-group17' : 'modp6144', 'dh-group18' : 'modp8192', 'dh-group19' : 'ecp256', 'dh-group20' : 'ecp384', 'dh-group21' : 'ecp521', 'dh-group22' : 'modp1024s160', 'dh-group23' : 'modp2048s224', 'dh-group24' : 'modp2048s256', 'dh-group25' : 'ecp192', 'dh-group26' : 'ecp224', 'dh-group27' : 'ecp224bp', 'dh-group28' : 'ecp256bp', 'dh-group29' : 'ecp384bp', 'dh-group30' : 'ecp512bp', 'dh-group31' : 'curve25519', 'dh-group32' : 'curve448' } ciphers = [] if 'proposal' in group_config: for priority, proposal in group_config['proposal'].items(): # both encryption and hash need to be specified for a proposal if not {'encryption', 'hash'} <= set(proposal): continue tmp = '{encryption}-{hash}'.format(**proposal) if 'prf' in proposal: tmp += '-' + proposal['prf'] if 'dh_group' in proposal: tmp += '-' + pfs_lut[ 'dh-group' + proposal['dh_group'] ] elif 'pfs' in group_config and group_config['pfs'] != 'disable': group = group_config['pfs'] if group_config['pfs'] == 'enable': group = get_first_ike_dh_group(ike_group) tmp += '-' + pfs_lut[group] ciphers.append(tmp) return ciphers @register_filter('get_uuid') def get_uuid(interface): """ Get interface IP addresses""" from uuid import uuid1 return uuid1() openvpn_translate = { 'des': 'des-cbc', '3des': 'des-ede3-cbc', 'bf128': 'bf-cbc', 'bf256': 'bf-cbc', 'aes128gcm': 'aes-128-gcm', 'aes128': 'aes-128-cbc', 'aes192gcm': 'aes-192-gcm', 'aes192': 'aes-192-cbc', 'aes256gcm': 'aes-256-gcm', 'aes256': 'aes-256-cbc' } @register_filter('openvpn_cipher') def get_openvpn_cipher(cipher): if cipher in openvpn_translate: return openvpn_translate[cipher].upper() return cipher.upper() @register_filter('openvpn_ncp_ciphers') def get_openvpn_ncp_ciphers(ciphers): out = [] for cipher in ciphers: if cipher in openvpn_translate: out.append(openvpn_translate[cipher]) else: out.append(cipher) return ':'.join(out).upper() @register_filter('snmp_auth_oid') def snmp_auth_oid(type): if type not in ['md5', 'sha', 'aes', 'des', 'none']: raise ValueError() OIDs = { 'md5' : '.1.3.6.1.6.3.10.1.1.2', 'sha' : '.1.3.6.1.6.3.10.1.1.3', 'aes' : '.1.3.6.1.6.3.10.1.2.4', 'des' : '.1.3.6.1.6.3.10.1.2.2', 'none': '.1.3.6.1.6.3.10.1.2.1' } return OIDs[type] @register_filter('nft_action') def nft_action(vyos_action): if vyos_action == 'accept': return 'return' return vyos_action @register_filter('nft_rule') def nft_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name='ip'): from vyos.firewall import parse_rule return parse_rule(rule_conf, fw_hook, fw_name, rule_id, ip_name) @register_filter('nft_default_rule') def nft_default_rule(fw_conf, fw_name, ipv6=False): output = ['counter'] default_action = fw_conf['default_action'] if 'enable_default_log' in fw_conf: action_suffix = default_action[:1].upper() output.append(f'log prefix "[{fw_name[:19]}-default-{action_suffix}]"') #output.append(nft_action(default_action)) output.append(f'{default_action}') if 'default_jump_target' in fw_conf: target = fw_conf['default_jump_target'] def_suffix = '6' if ipv6 else '' output.append(f'NAME{def_suffix}_{target}') output.append(f'comment "{fw_name} default-action {default_action}"') return " ".join(output) @register_filter('nft_state_policy') def nft_state_policy(conf, state): out = [f'ct state {state}'] if 'log' in conf and 'enable' in conf['log']: log_state = state[:3].upper() log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper() out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"') if 'log_level' in conf: log_level = conf['log_level'] out.append(f'level {log_level}') out.append('counter') if 'action' in conf: out.append(conf['action']) return " ".join(out) @register_filter('nft_intra_zone_action') def nft_intra_zone_action(zone_conf, ipv6=False): if 'intra_zone_filtering' in zone_conf: intra_zone = zone_conf['intra_zone_filtering'] fw_name = 'ipv6_name' if ipv6 else 'name' name_prefix = 'NAME6_' if ipv6 else 'NAME_' if 'action' in intra_zone: if intra_zone['action'] == 'accept': return 'return' return intra_zone['action'] elif dict_search_args(intra_zone, 'firewall', fw_name): name = dict_search_args(intra_zone, 'firewall', fw_name) return f'jump {name_prefix}{name}' return 'return' @register_filter('nft_nested_group') def nft_nested_group(out_list, includes, groups, key): if not vyos_defined(out_list): out_list = [] def add_includes(name): if key in groups[name]: for item in groups[name][key]: if item in out_list: continue out_list.append(item) if 'include' in groups[name]: for name_inc in groups[name]['include']: add_includes(name_inc) for name in includes: add_includes(name) return out_list @register_filter('nat_rule') def nat_rule(rule_conf, rule_id, nat_type, ipv6=False): from vyos.nat import parse_nat_rule return parse_nat_rule(rule_conf, rule_id, nat_type, ipv6) @register_filter('nat_static_rule') def nat_static_rule(rule_conf, rule_id, nat_type): from vyos.nat import parse_nat_static_rule return parse_nat_static_rule(rule_conf, rule_id, nat_type) @register_filter('conntrack_ignore_rule') def conntrack_ignore_rule(rule_conf, rule_id, ipv6=False): ip_prefix = 'ip6' if ipv6 else 'ip' def_suffix = '6' if ipv6 else '' output = [] if 'inbound_interface' in rule_conf: ifname = rule_conf['inbound_interface'] if ifname != 'any': output.append(f'iifname {ifname}') if 'protocol' in rule_conf: proto = rule_conf['protocol'] output.append(f'meta l4proto {proto}') + tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') + if tcp_flags: + from vyos.firewall import parse_tcp_flags + output.append(parse_tcp_flags(tcp_flags)) + for side in ['source', 'destination']: if side in rule_conf: side_conf = rule_conf[side] prefix = side[0] if 'address' in side_conf: address = side_conf['address'] operator = '' if address[0] == '!': operator = '!=' address = address[1:] output.append(f'{ip_prefix} {prefix}addr {operator} {address}') if 'port' in side_conf: port = side_conf['port'] operator = '' if port[0] == '!': operator = '!=' port = port[1:] output.append(f'th {prefix}port {operator} {port}') if 'group' in side_conf: group = side_conf['group'] if 'address_group' in group: group_name = group['address_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_prefix} {prefix}addr {operator} @A{def_suffix}_{group_name}') # Generate firewall group domain-group elif 'domain_group' in group: group_name = group['domain_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}') elif 'network_group' in group: group_name = group['network_group'] operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{ip_prefix} {prefix}addr {operator} @N{def_suffix}_{group_name}') if 'port_group' in group: group_name = group['port_group'] if proto == 'tcp_udp': proto = 'th' operator = '' if group_name[0] == '!': operator = '!=' group_name = group_name[1:] output.append(f'{proto} {prefix}port {operator} @P_{group_name}') output.append('counter notrack') output.append(f'comment "ignore-{rule_id}"') return " ".join(output) @register_filter('range_to_regex') def range_to_regex(num_range): """Convert range of numbers or list of ranges to regex % range_to_regex('11-12') '(1[1-2])' % range_to_regex(['11-12', '14-15']) '(1[1-2]|1[4-5])' """ from vyos.range_regex import range_to_regex if isinstance(num_range, list): data = [] for entry in num_range: if '-' not in entry: data.append(entry) else: data.append(range_to_regex(entry)) return f'({"|".join(data)})' if '-' not in num_range: return num_range regex = range_to_regex(num_range) return f'({regex})' @register_test('vyos_defined') def vyos_defined(value, test_value=None, var_type=None): """ Jinja2 plugin to test if a variable is defined and not none - vyos_defined will test value if defined and is not none and return true or false. If test_value is supplied, the value must also pass == test_value to return true. If var_type is supplied, the value must also be of the specified class/type Examples: 1. Test if var is defined and not none: {% if foo is vyos_defined %} ... {% endif %} 2. Test if variable is defined, not none and has value "something" {% if bar is vyos_defined("something") %} ... {% endif %} Parameters ---------- value : any Value to test from ansible test_value : any, optional Value to test in addition of defined and not none, by default None var_type : ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'], optional Type or Class to test for Returns ------- boolean True if variable matches criteria, False in other cases. Implementation inspired and re-used from https://github.com/aristanetworks/ansible-avd/ """ from jinja2 import Undefined if isinstance(value, Undefined) or value is None: # Invalid value - return false return False elif test_value is not None and value != test_value: # Valid value but not matching the optional argument return False elif str(var_type).lower() in ['float', 'int', 'str', 'list', 'dict', 'tuple', 'bool'] and str(var_type).lower() != type(value).__name__: # Invalid class - return false return False else: # Valid value and is matching optional argument if provided - return true return True diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 391ef03ff..75d6e9bb1 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -1,619 +1,619 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2022 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 unittest from glob import glob from time import sleep from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd from vyos.utils.process import run sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'default': '0', 'test_value': 'disable'}, 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'default': '1', 'test_value': 'enable'}, 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route', 'default': '0', 'test_value': 'enable'}, 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'default': '-1', 'test_value': 'enable'}, 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians', 'default': '1', 'test_value': 'disable'}, 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects', 'default': '0', 'test_value': 'enable'}, 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects', 'default': '1', 'test_value': 'disable'}, 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies', 'default': '1', 'test_value': 'disable'}, 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337', 'default': '0', 'test_value': 'enable'} } class TestFirewall(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestFirewall, cls).setUpClass() # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, ['firewall']) @classmethod def tearDownClass(cls): super(TestFirewall, cls).tearDownClass() def tearDown(self): self.cli_delete(['firewall']) self.cli_commit() # Verify chains/sets are cleaned up from nftables nftables_search = [ ['set M_smoketest_mac'], ['set N_smoketest_network'], ['set P_smoketest_port'], ['set D_smoketest_domain'], ['set RECENT_smoketest_4'], ['chain NAME_smoketest'] ] self.verify_nftables(nftables_search, 'ip vyos_filter', inverse=True) def verify_nftables(self, nftables_search, table, inverse=False, args=''): nftables_output = cmd(f'sudo nft {args} list table {table}') for search in nftables_search: matched = False for line in nftables_output.split("\n"): if all(item in line for item in search): matched = True break self.assertTrue(not matched if inverse else matched, msg=search) def verify_nftables_chain(self, nftables_search, table, chain, inverse=False, args=''): nftables_output = cmd(f'sudo nft {args} list chain {table} {chain}') for search in nftables_search: matched = False for line in nftables_output.split("\n"): if all(item in line for item in search): matched = True break self.assertTrue(not matched if inverse else matched, msg=search) def wait_for_domain_resolver(self, table, set_name, element, max_wait=10): # Resolver no longer blocks commit, need to wait for daemon to populate set count = 0 while count < max_wait: code = run(f'sudo nft get element {table} {set_name} {{ {element} }}') if code == 0: return True count += 1 sleep(1) return False def test_geoip(self): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match']) self.cli_commit() nftables_search = [ ['ip saddr @GEOIP_CC_name_smoketest_1', 'drop'], ['ip saddr != @GEOIP_CC_name_smoketest_2', 'accept'] ] # -t prevents 1000+ GeoIP elements being returned self.verify_nftables(nftables_search, 'ip vyos_filter', args='-t') def test_groups(self): hostmap_path = ['system', 'static-host-mapping', 'host-name'] example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11'] self.cli_set(hostmap_path + ['example.com', 'inet', '192.0.2.5']) for ips in example_org: self.cli_set(hostmap_path + ['example.org', 'inet', ips]) self.cli_commit() self.cli_set(['firewall', 'group', 'mac-group', 'smoketest_mac', 'mac-address', '00:01:02:03:04:05']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '123']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'address', '172.16.10.10']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'protocol', 'tcp_udp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '2', 'source', 'group', 'mac-group', 'smoketest_mac']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'source', 'group', 'domain-group', 'smoketest_domain']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'interface-group', '!smoketest_interface']) self.cli_commit() self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5') nftables_search = [ ['ip saddr @N_smoketest_network', 'ip daddr 172.16.10.10', 'th dport @P_smoketest_port', 'accept'], ['elements = { 172.16.99.0/24 }'], ['elements = { 53, 123 }'], ['ether saddr @M_smoketest_mac', 'accept'], ['elements = { 00:01:02:03:04:05 }'], ['set D_smoketest_domain'], ['elements = { 192.0.2.5, 192.0.2.8,'], ['192.0.2.10, 192.0.2.11 }'], ['ip saddr @D_smoketest_domain', 'accept'], ['oifname != @I_smoketest_interface', 'accept'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') self.cli_delete(['system', 'static-host-mapping']) self.cli_commit() def test_nested_groups(self): self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'network', '172.16.99.0/24']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'network', '172.16.101.0/24']) self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network1', 'include', 'smoketest_network']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port', 'port', '53']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'port', '123']) self.cli_set(['firewall', 'group', 'port-group', 'smoketest_port1', 'include', 'smoketest_port']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'source', 'group', 'network-group', 'smoketest_network1']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'destination', 'group', 'port-group', 'smoketest_port1']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp']) self.cli_commit() # Test circular includes self.cli_set(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(['firewall', 'group', 'network-group', 'smoketest_network', 'include', 'smoketest_network1']) nftables_search = [ ['ip saddr @N_smoketest_network1', 'th dport @P_smoketest_port1', 'accept'], ['elements = { 172.16.99.0/24, 172.16.101.0/24 }'], ['elements = { 53, 123 }'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv4_basic_rules(self): name = 'smoketest' interface = 'eth0' interface_inv = '!eth0' interface_wc = 'l2tp*' mss_range = '501-1460' conn_mark = '555' self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'source', 'address', '172.16.20.10']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '172.16.10.10']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'log-options', 'level', 'debug']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'ttl', 'eq', '15']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'destination', 'port', '8888']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'log-options', 'level', 'err']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'tcp', 'flags', 'not', 'ack']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'ttl', 'gt', '102']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'destination', 'port', '22']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'limit', 'rate', '5/minute']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '3', 'log', 'disable']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'destination', 'port', '22']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'count', '10']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'recent', 'time', 'minute']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'packet-type', 'host']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'flags', 'syn']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'tcp', 'mss', mss_range]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'packet-type', 'broadcast']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '5', 'inbound-interface', 'interface-name', interface_wc]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'action', 'return']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'protocol', 'gre']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '6', 'connection-mark', conn_mark]) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'default-action', 'accept']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'protocol', 'gre']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '5', 'outbound-interface', 'interface-name', interface_inv]) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'action', 'return']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'protocol', 'icmp']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '6', 'connection-mark', conn_mark]) self.cli_commit() mark_hex = "{0:#010x}".format(int(conn_mark)) nftables_search = [ ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy drop;'], ['tcp dport 22', 'limit rate 5/minute', 'accept'], ['tcp dport 22', 'add @RECENT_FWD_filter_4 { ip saddr limit rate over 10/minute burst 10 packets }', 'meta pkttype host', 'drop'], ['chain VYOS_INPUT_filter'], ['type filter hook input priority filter; policy accept;'], ['tcp flags & syn == syn', f'tcp option maxseg size {mss_range}', f'iifname "{interface_wc}"', 'meta pkttype broadcast', 'accept'], ['meta l4proto gre', f'ct mark {mark_hex}', 'return'], ['chain VYOS_OUTPUT_filter'], ['type filter hook output priority filter; policy accept;'], ['meta l4proto gre', f'oifname != "{interface}"', 'drop'], ['meta l4proto icmp', f'ct mark {mark_hex}', 'return'], ['chain NAME_smoketest'], ['saddr 172.16.20.10', 'daddr 172.16.10.10', 'log prefix "[ipv4-NAM-smoketest-1-A]" log level debug', 'ip ttl 15', 'accept'], ['tcp flags syn / syn,ack', 'tcp dport 8888', 'log prefix "[ipv4-NAM-smoketest-2-R]" log level err', 'ip ttl > 102', 'reject'], ['log prefix "[smoketest-default-D]"','smoketest default-action', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv4_advanced(self): name = 'smoketest-adv' name2 = 'smoketest-adv2' interface = 'eth0' self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '64']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '512']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'packet-length', '1024']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '17']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'dscp', '52']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'group', '66']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'snapshot-length', '6666']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '6', 'log-options', 'queue-threshold','32000']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length', '1-30000']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'packet-length-exclude', '60000-65535']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp', '3-11']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '7', 'dscp-exclude', '21-25']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'source', 'address', '198.51.100.1']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', name]) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'protocol', 'tcp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'action', 'queue']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '2', 'queue', '3']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'protocol', 'udp']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'action', 'queue']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'fanout']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue-options', 'bypass']) self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '3', 'queue', '0-15']) self.cli_commit() nftables_search = [ ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy drop;'], ['ip saddr 198.51.100.1', f'jump NAME_{name}'], ['chain VYOS_INPUT_filter'], ['type filter hook input priority filter; policy accept;'], [f'meta l4proto tcp','queue to 3'], [f'meta l4proto udp','queue flags bypass,fanout to 0-15'], [f'chain NAME_{name}'], ['ip length { 64, 512, 1024 }', 'ip dscp { 0x11, 0x34 }', f'log prefix "[ipv4-NAM-{name}-6-A]" log group 66 snaplen 6666 queue-threshold 32000', 'accept'], ['ip length 1-30000', 'ip length != 60000-65535', 'ip dscp 0x03-0x0b', 'ip dscp != 0x15-0x19', 'accept'], [f'log prefix "[{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv4_mask(self): name = 'smoketest-mask' interface = 'eth0' self.cli_set(['firewall', 'group', 'address-group', 'mask_group', 'address', '1.1.1.1']) self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address', '0.0.1.2']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'destination', 'address-mask', '0.0.255.255']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address', '!0.0.3.4']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'source', 'address-mask', '0.0.255.255']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'source', 'address-mask', '0.0.255.255']) self.cli_commit() nftables_search = [ [f'daddr & 0.0.255.255 == 0.0.1.2'], [f'saddr & 0.0.255.255 != 0.0.3.4'], [f'saddr & 0.0.255.255 == @A_mask_group'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') def test_ipv6_basic_rules(self): name = 'v6-smoketest' interface = 'eth0' self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'source', 'address', '2002::1']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '2002::1:1']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log', 'enable']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'log-options', 'level', 'crit']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'default-action', 'accept']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'protocol', 'tcp_udp']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'destination', 'port', '8888']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '2', 'inbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'action', 'return']) self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'protocol', 'gre']) self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '3', 'outbound-interface', 'interface-name', interface]) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'protocol', 'udp']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'source', 'address', '2002::1:2']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '3', 'inbound-interface', 'interface-name', interface]) self.cli_commit() nftables_search = [ ['chain VYOS_IPV6_FORWARD_filter'], ['type filter hook forward priority filter; policy accept;'], ['meta l4proto { tcp, udp }', 'th dport 8888', f'iifname "{interface}"', 'reject'], ['chain VYOS_IPV6_INPUT_filter'], ['type filter hook input priority filter; policy accept;'], ['meta l4proto udp', 'ip6 saddr 2002::1:2', f'iifname "{interface}"', 'accept'], ['chain VYOS_IPV6_OUTPUT_filter'], ['type filter hook output priority filter; policy drop;'], ['meta l4proto gre', f'oifname "{interface}"', 'return'], [f'chain NAME6_{name}'], ['saddr 2002::1', 'daddr 2002::1:1', 'log prefix "[ipv6-NAM-v6-smoketest-1-A]" log level crit', 'accept'], [f'"{name} default-action drop"', f'log prefix "[{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv6_advanced(self): name = 'v6-smoketest-adv' name2 = 'v6-smoketest-adv2' interface = 'eth0' self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '65']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '513']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'packet-length', '1025']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '18']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'dscp', '53']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length', '1-1999']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'packet-length-exclude', '60000-65535']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp', '4-14']) self.cli_set(['firewall', 'ipv6', 'forward', 'filter', 'rule', '4', 'dscp-exclude', '31-35']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'default-action', 'accept']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'source', 'address', '2001:db8::/64']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '1', 'jump-target', name]) self.cli_commit() nftables_search = [ ['chain VYOS_IPV6_FORWARD_filter'], ['type filter hook forward priority filter; policy accept;'], ['ip6 length 1-1999', 'ip6 length != 60000-65535', 'ip6 dscp 0x04-0x0e', 'ip6 dscp != 0x1f-0x23', 'accept'], ['chain VYOS_IPV6_INPUT_filter'], ['type filter hook input priority filter; policy accept;'], ['ip6 saddr 2001:db8::/64', f'jump NAME6_{name}'], [f'chain NAME6_{name}'], ['ip6 length { 65, 513, 1025 }', 'ip6 dscp { af21, 0x35 }', 'accept'], [f'log prefix "[{name}-default-D]"', 'drop'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv6_mask(self): name = 'v6-smoketest-mask' interface = 'eth0' self.cli_set(['firewall', 'group', 'ipv6-address-group', 'mask_group', 'address', '::beef']) self.cli_set(['firewall', 'ipv6', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address', '::1111:2222:3333:4444']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '1', 'destination', 'address-mask', '::ffff:ffff:ffff:ffff']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'action', 'accept']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address', '!::aaaa:bbbb:cccc:dddd']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '2', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'action', 'drop']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'group', 'address-group', 'mask_group']) self.cli_set(['firewall', 'ipv6', 'name', name, 'rule', '3', 'source', 'address-mask', '::ffff:ffff:ffff:ffff']) self.cli_commit() nftables_search = [ ['daddr & ::ffff:ffff:ffff:ffff == ::1111:2222:3333:4444'], ['saddr & ::ffff:ffff:ffff:ffff != ::aaaa:bbbb:cccc:dddd'], ['saddr & ::ffff:ffff:ffff:ffff == @A6_mask_group'] ] self.verify_nftables(nftables_search, 'ip6 vyos_filter') def test_ipv4_state_and_status_rules(self): name = 'smoketest-state' interface = 'eth0' self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'related', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'action', 'reject']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '2', 'state', 'invalid', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'state', 'new', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '3', 'connection-status', 'nat', 'destination']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'new', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'state', 'established', 'enable']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '4', 'connection-status', 'nat', 'source']) self.cli_commit() nftables_search = [ ['ct state { established, related }', 'accept'], ['ct state invalid', 'reject'], ['ct state new', 'ct status dnat', 'accept'], ['ct state { established, new }', 'ct status snat', 'accept'], ['drop', f'comment "{name} default-action drop"'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') # Check conntrack - self.verify_nftables_chain([['accept']], 'raw', 'FW_CONNTRACK') - self.verify_nftables_chain([['return']], 'ip6 raw', 'FW_CONNTRACK') + self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') + self.verify_nftables_chain([['return']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') def test_bridge_basic_rules(self): name = 'smoketest' interface_in = 'eth0' mac_address = '00:53:00:00:00:01' vlan_id = '12' vlan_prior = '3' self.cli_set(['firewall', 'bridge', 'name', name, 'default-action', 'accept']) self.cli_set(['firewall', 'bridge', 'name', name, 'enable-default-log']) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'source', 'mac-address', mac_address]) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'inbound-interface', 'interface-name', interface_in]) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log', 'enable']) self.cli_set(['firewall', 'bridge', 'name', name, 'rule', '1', 'log-options', 'level', 'crit']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'default-action', 'drop']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '1', 'vlan', 'id', vlan_id]) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'action', 'jump']) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'jump-target', name]) self.cli_set(['firewall', 'bridge', 'forward', 'filter', 'rule', '2', 'vlan', 'priority', vlan_prior]) self.cli_commit() nftables_search = [ ['chain VYOS_FORWARD_filter'], ['type filter hook forward priority filter; policy drop;'], [f'vlan id {vlan_id}', 'accept'], [f'vlan pcp {vlan_prior}', f'jump NAME_{name}'], [f'chain NAME_{name}'], [f'ether saddr {mac_address}', f'iifname "{interface_in}"', f'log prefix "[bri-NAM-{name}-1-A]" log level crit', 'accept'] ] self.verify_nftables(nftables_search, 'bridge vyos_filter') def test_source_validation(self): # Strict self.cli_set(['firewall', 'global-options', 'source-validation', 'strict']) self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'strict']) self.cli_commit() nftables_strict_search = [ ['fib saddr . iif oif 0', 'drop'] ] self.verify_nftables_chain(nftables_strict_search, 'ip raw', 'vyos_global_rpfilter') self.verify_nftables_chain(nftables_strict_search, 'ip6 raw', 'vyos_global_rpfilter') # Loose self.cli_set(['firewall', 'global-options', 'source-validation', 'loose']) self.cli_set(['firewall', 'global-options', 'ipv6-source-validation', 'loose']) self.cli_commit() nftables_loose_search = [ ['fib saddr oif 0', 'drop'] ] self.verify_nftables_chain(nftables_loose_search, 'ip raw', 'vyos_global_rpfilter') self.verify_nftables_chain(nftables_loose_search, 'ip6 raw', 'vyos_global_rpfilter') def test_sysfs(self): for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) for path in paths: with open(path, 'r') as f: self.assertEqual(f.read().strip(), conf['default'], msg=path) self.cli_set(['firewall', 'global-options', name.replace("_", "-"), conf['test_value']]) self.cli_commit() for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) for path in paths: with open(path, 'r') as f: self.assertNotEqual(f.read().strip(), conf['default'], msg=path) def test_flow_offload_software(self): self.cli_set(['firewall', 'global-options', 'flow-offload', 'software', 'interface', 'eth0']) self.cli_commit() nftables_search = [ ['flowtable VYOS_FLOWTABLE_software'], ['hook ingress priority filter - 1'], ['devices = { eth0 }'], ['flow add @VYOS_FLOWTABLE_software'], ] self.verify_nftables(nftables_search, 'inet vyos_offload') if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index ea304783d..c9f184558 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -1,293 +1,294 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2023 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 import re import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.firewall import find_nftables_rule from vyos.utils.process import cmd from vyos.utils.file import read_file base_path = ['system', 'conntrack'] def get_sysctl(parameter): tmp = parameter.replace(r'.', r'/') return read_file(f'/proc/sys/{tmp}') class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() def verify_nftables(self, nftables_search, table, inverse=False, args=''): nftables_output = cmd(f'sudo nft {args} list table {table}') for search in nftables_search: matched = False for line in nftables_output.split("\n"): if all(item in line for item in search): matched = True break self.assertTrue(not matched if inverse else matched, msg=search) def test_conntrack_options(self): conntrack_config = { 'net.netfilter.nf_conntrack_expect_max' : { 'cli' : ['expect-table-size'], 'test_value' : '8192', 'default_value' : '2048', }, 'net.nf_conntrack_max' :{ 'cli' : ['table-size'], 'test_value' : '500000', 'default_value' : '262144', }, 'net.ipv4.tcp_max_syn_backlog' :{ 'cli' : ['tcp', 'half-open-connections'], 'test_value' : '2048', 'default_value' : '512', }, 'net.netfilter.nf_conntrack_tcp_loose' :{ 'cli' : ['tcp', 'loose'], 'test_value' : 'disable', 'default_value' : '1', }, 'net.netfilter.nf_conntrack_tcp_max_retrans' :{ 'cli' : ['tcp', 'max-retrans'], 'test_value' : '128', 'default_value' : '3', }, 'net.netfilter.nf_conntrack_icmp_timeout' :{ 'cli' : ['timeout', 'icmp'], 'test_value' : '180', 'default_value' : '30', }, 'net.netfilter.nf_conntrack_generic_timeout' :{ 'cli' : ['timeout', 'other'], 'test_value' : '1200', 'default_value' : '600', }, 'net.netfilter.nf_conntrack_tcp_timeout_close_wait' :{ 'cli' : ['timeout', 'tcp', 'close-wait'], 'test_value' : '30', 'default_value' : '60', }, 'net.netfilter.nf_conntrack_tcp_timeout_close' :{ 'cli' : ['timeout', 'tcp', 'close'], 'test_value' : '20', 'default_value' : '10', }, 'net.netfilter.nf_conntrack_tcp_timeout_established' :{ 'cli' : ['timeout', 'tcp', 'established'], 'test_value' : '1000', 'default_value' : '432000', }, 'net.netfilter.nf_conntrack_tcp_timeout_fin_wait' :{ 'cli' : ['timeout', 'tcp', 'fin-wait'], 'test_value' : '240', 'default_value' : '120', }, 'net.netfilter.nf_conntrack_tcp_timeout_last_ack' :{ 'cli' : ['timeout', 'tcp', 'last-ack'], 'test_value' : '300', 'default_value' : '30', }, 'net.netfilter.nf_conntrack_tcp_timeout_syn_recv' :{ 'cli' : ['timeout', 'tcp', 'syn-recv'], 'test_value' : '100', 'default_value' : '60', }, 'net.netfilter.nf_conntrack_tcp_timeout_syn_sent' :{ 'cli' : ['timeout', 'tcp', 'syn-sent'], 'test_value' : '300', 'default_value' : '120', }, 'net.netfilter.nf_conntrack_tcp_timeout_time_wait' :{ 'cli' : ['timeout', 'tcp', 'time-wait'], 'test_value' : '303', 'default_value' : '120', }, 'net.netfilter.nf_conntrack_udp_timeout' :{ 'cli' : ['timeout', 'udp', 'other'], 'test_value' : '90', 'default_value' : '30', }, 'net.netfilter.nf_conntrack_udp_timeout_stream' :{ 'cli' : ['timeout', 'udp', 'stream'], 'test_value' : '200', 'default_value' : '180', }, } for parameter, parameter_config in conntrack_config.items(): self.cli_set(base_path + parameter_config['cli'] + [parameter_config['test_value']]) # commit changes self.cli_commit() # validate configuration for parameter, parameter_config in conntrack_config.items(): tmp = parameter_config['test_value'] # net.netfilter.nf_conntrack_tcp_loose has a fancy "disable" value, # make this work if tmp == 'disable': tmp = '0' self.assertEqual(get_sysctl(f'{parameter}'), tmp) # delete all configuration options and revert back to defaults self.cli_delete(base_path) self.cli_commit() # validate configuration for parameter, parameter_config in conntrack_config.items(): self.assertEqual(get_sysctl(f'{parameter}'), parameter_config['default_value']) def test_conntrack_module_enable(self): # conntrack helper modules are disabled by default modules = { 'ftp' : { 'driver' : ['nf_nat_ftp', 'nf_conntrack_ftp'], }, 'h323' : { 'driver' : ['nf_nat_h323', 'nf_conntrack_h323'], }, 'nfs' : { 'nftables' : ['ct helper set "rpc_tcp"', 'ct helper set "rpc_udp"'] }, 'pptp' : { 'driver' : ['nf_nat_pptp', 'nf_conntrack_pptp'], }, 'sip' : { 'driver' : ['nf_nat_sip', 'nf_conntrack_sip'], }, 'sqlnet' : { 'nftables' : ['ct helper set "tns_tcp"'] }, 'tftp' : { 'driver' : ['nf_nat_tftp', 'nf_conntrack_tftp'], }, } # load modules for module in modules: self.cli_set(base_path + ['modules', module]) # commit changes self.cli_commit() # verify modules are loaded on the system for module, module_options in modules.items(): if 'driver' in module_options: for driver in module_options['driver']: self.assertTrue(os.path.isdir(f'/sys/module/{driver}')) if 'nftables' in module_options: for rule in module_options['nftables']: - self.assertTrue(find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) != None) + self.assertTrue(find_nftables_rule('ip vyos_conntrack', 'VYOS_CT_HELPER', [rule]) != None) # unload modules for module in modules: self.cli_delete(base_path + ['modules', module]) # commit changes self.cli_commit() # verify modules are not loaded on the system for module, module_options in modules.items(): if 'driver' in module_options: for driver in module_options['driver']: self.assertFalse(os.path.isdir(f'/sys/module/{driver}')) if 'nftables' in module_options: for rule in module_options['nftables']: - self.assertTrue(find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule]) == None) + self.assertTrue(find_nftables_rule('ip vyos_conntrack', 'VYOS_CT_HELPER', [rule]) == None) def test_conntrack_hash_size(self): hash_size = '65536' hash_size_default = '32768' self.cli_set(base_path + ['hash-size', hash_size]) # commit changes self.cli_commit() # verify new configuration - only effective after reboot, but # a valid config file is sufficient tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf') self.assertIn(hash_size, tmp) # Test default value by deleting the configuration self.cli_delete(base_path + ['hash-size']) # commit changes self.cli_commit() # verify new configuration - only effective after reboot, but # a valid config file is sufficient tmp = read_file('/etc/modprobe.d/vyatta_nf_conntrack.conf') self.assertIn(hash_size_default, tmp) def test_conntrack_ignore(self): address_group = 'conntracktest' address_group_member = '192.168.0.1' ipv6_address_group = 'conntracktest6' ipv6_address_group_member = 'dead:beef::1' self.cli_set(['firewall', 'group', 'address-group', address_group, 'address', address_group_member]) self.cli_set(['firewall', 'group', 'ipv6-address-group', ipv6_address_group, 'address', ipv6_address_group_member]) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'source', 'address', '192.0.2.1']) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'destination', 'address', '192.0.2.2']) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'destination', 'port', '22']) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'protocol', 'tcp']) + self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '1', 'tcp', 'flags', 'syn']) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'source', 'address', '192.0.2.1']) self.cli_set(base_path + ['ignore', 'ipv4', 'rule', '2', 'destination', 'group', 'address-group', address_group]) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'source', 'address', 'fe80::1']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'address', 'fe80::2']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'destination', 'port', '22']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '11', 'protocol', 'tcp']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '12', 'source', 'address', 'fe80::1']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '12', 'destination', 'group', 'address-group', ipv6_address_group]) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '13', 'source', 'address', 'fe80::1']) self.cli_set(base_path + ['ignore', 'ipv6', 'rule', '13', 'destination', 'address', '!fe80::3']) self.cli_commit() nftables_search = [ - ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'notrack'], + ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'tcp flags & syn == syn', 'notrack'], ['ip saddr 192.0.2.1', 'ip daddr @A_conntracktest', 'notrack'] ] nftables6_search = [ ['ip6 saddr fe80::1', 'ip6 daddr fe80::2', 'tcp dport 22', 'notrack'], ['ip6 saddr fe80::1', 'ip6 daddr @A6_conntracktest6', 'notrack'], ['ip6 saddr fe80::1', 'ip6 daddr != fe80::3', 'notrack'] ] - self.verify_nftables(nftables_search, 'raw') - self.verify_nftables(nftables6_search, 'ip6 raw') + self.verify_nftables(nftables_search, 'ip vyos_conntrack') + self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack') self.cli_delete(['firewall']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_flow-accounting.py b/smoketest/scripts/cli/test_system_flow-accounting.py index d55ea616e..6c761579b 100755 --- a/smoketest/scripts/cli/test_system_flow-accounting.py +++ b/smoketest/scripts/cli/test_system_flow-accounting.py @@ -1,296 +1,296 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2022 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 unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section from vyos.template import bracketize_ipv6 from vyos.template import is_ipv6 from vyos.utils.process import cmd from vyos.utils.process import process_named_running from vyos.utils.file import read_file PROCESS_NAME = 'uacctd' base_path = ['system', 'flow-accounting'] uacctd_conf = '/run/pmacct/uacctd.conf' class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestSystemFlowAccounting, cls).setUpClass() # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) def tearDown(self): # after service removal process must no longer run self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) self.cli_commit() # after service removal process must no longer run self.assertFalse(process_named_running(PROCESS_NAME)) def test_basic(self): buffer_size = '5' # MiB syslog = 'all' self.cli_set(base_path + ['buffer-size', buffer_size]) self.cli_set(base_path + ['syslog-facility', syslog]) # You need to configure at least one interface for flow-accounting with self.assertRaises(ConfigSessionError): self.cli_commit() for interface in Section.interfaces('ethernet'): self.cli_set(base_path + ['interface', interface]) # commit changes self.cli_commit() # verify configuration - nftables_output = cmd('sudo nft list chain raw VYOS_CT_PREROUTING_HOOK').splitlines() + nftables_output = cmd('sudo nft list chain raw VYOS_PREROUTING_HOOK').splitlines() for interface in Section.interfaces('ethernet'): rule_found = False ifname_search = f'iifname "{interface}"' for nftables_line in nftables_output: if 'FLOW_ACCOUNTING_RULE' in nftables_line and ifname_search in nftables_line: self.assertIn('group 2', nftables_line) self.assertIn('snaplen 128', nftables_line) self.assertIn('queue-threshold 100', nftables_line) rule_found = True break self.assertTrue(rule_found) uacctd = read_file(uacctd_conf) # circular queue size - buffer_size tmp = int(buffer_size) *1024 *1024 self.assertIn(f'plugin_pipe_size: {tmp}', uacctd) # transfer buffer size - recommended value from pmacct developers 1/1000 of pipe size tmp = int(buffer_size) *1024 *1024 # do an integer division tmp //= 1000 self.assertIn(f'plugin_buffer_size: {tmp}', uacctd) # when 'disable-imt' is not configured on the CLI it must be present self.assertIn(f'imt_path: /tmp/uacctd.pipe', uacctd) self.assertIn(f'imt_mem_pools_number: 169', uacctd) self.assertIn(f'syslog: {syslog}', uacctd) self.assertIn(f'plugins: memory', uacctd) def test_sflow(self): sampling_rate = '4000' source_address = '192.0.2.1' dummy_if = 'dum3841' agent_address = '192.0.2.2' sflow_server = { '1.2.3.4' : { }, '5.6.7.8' : { 'port' : '6000' }, } self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32']) self.cli_set(base_path + ['disable-imt']) # You need to configure at least one interface for flow-accounting with self.assertRaises(ConfigSessionError): self.cli_commit() for interface in Section.interfaces('ethernet'): self.cli_set(base_path + ['interface', interface]) # You need to configure at least one sFlow or NetFlow protocol, or not # set "disable-imt" for flow-accounting with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['sflow', 'agent-address', agent_address]) self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate]) self.cli_set(base_path + ['sflow', 'source-address', source_address]) for server, server_config in sflow_server.items(): self.cli_set(base_path + ['sflow', 'server', server]) if 'port' in server_config: self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']]) # commit changes self.cli_commit() uacctd = read_file(uacctd_conf) # when 'disable-imt' is not configured on the CLI it must be present self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd) self.assertNotIn(f'imt_mem_pools_number: 169', uacctd) self.assertNotIn(f'plugins: memory', uacctd) for server, server_config in sflow_server.items(): plugin_name = server.replace('.', '-') if 'port' in server_config: self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}', uacctd) else: self.assertIn(f'sfprobe_receiver[sf_{plugin_name}]: {server}:6343', uacctd) self.assertIn(f'sfprobe_agentip[sf_{plugin_name}]: {agent_address}', uacctd) self.assertIn(f'sampling_rate[sf_{plugin_name}]: {sampling_rate}', uacctd) self.assertIn(f'sfprobe_source_ip[sf_{plugin_name}]: {source_address}', uacctd) self.cli_delete(['interfaces', 'dummy', dummy_if]) def test_sflow_ipv6(self): sampling_rate = '100' sflow_server = { '2001:db8::1' : { }, '2001:db8::2' : { 'port' : '6000' }, } self.cli_set(base_path + ['disable-imt']) # You need to configure at least one interface for flow-accounting with self.assertRaises(ConfigSessionError): self.cli_commit() for interface in Section.interfaces('ethernet'): self.cli_set(base_path + ['interface', interface]) # You need to configure at least one sFlow or NetFlow protocol, or not # set "disable-imt" for flow-accounting with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_set(base_path + ['sflow', 'sampling-rate', sampling_rate]) for server, server_config in sflow_server.items(): self.cli_set(base_path + ['sflow', 'server', server]) if 'port' in server_config: self.cli_set(base_path + ['sflow', 'server', server, 'port', server_config['port']]) # commit changes self.cli_commit() uacctd = read_file(uacctd_conf) # when 'disable-imt' is not configured on the CLI it must be present self.assertNotIn(f'imt_path: /tmp/uacctd.pipe', uacctd) self.assertNotIn(f'imt_mem_pools_number: 169', uacctd) self.assertNotIn(f'plugins: memory', uacctd) for server, server_config in sflow_server.items(): tmp_srv = server tmp_srv = tmp_srv.replace(':', '-') if 'port' in server_config: self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd) else: self.assertIn(f'sfprobe_receiver[sf_{tmp_srv}]: {bracketize_ipv6(server)}:6343', uacctd) self.assertIn(f'sampling_rate[sf_{tmp_srv}]: {sampling_rate}', uacctd) def test_netflow(self): engine_id = '33' max_flows = '667' sampling_rate = '100' source_address = '192.0.2.1' dummy_if = 'dum3842' agent_address = '192.0.2.10' version = '10' tmo_expiry = '120' tmo_flow = '1200' tmo_icmp = '60' tmo_max = '50000' tmo_tcp_fin = '100' tmo_tcp_generic = '120' tmo_tcp_rst = '99' tmo_udp = '10' netflow_server = { '11.22.33.44' : { }, '55.66.77.88' : { 'port' : '6000' }, '2001:db8::1' : { }, } self.cli_set(['interfaces', 'dummy', dummy_if, 'address', agent_address + '/32']) self.cli_set(['interfaces', 'dummy', dummy_if, 'address', source_address + '/32']) for interface in Section.interfaces('ethernet'): self.cli_set(base_path + ['interface', interface]) self.cli_set(base_path + ['netflow', 'engine-id', engine_id]) self.cli_set(base_path + ['netflow', 'max-flows', max_flows]) self.cli_set(base_path + ['netflow', 'sampling-rate', sampling_rate]) self.cli_set(base_path + ['netflow', 'source-address', source_address]) self.cli_set(base_path + ['netflow', 'version', version]) # timeouts self.cli_set(base_path + ['netflow', 'timeout', 'expiry-interval', tmo_expiry]) self.cli_set(base_path + ['netflow', 'timeout', 'flow-generic', tmo_flow]) self.cli_set(base_path + ['netflow', 'timeout', 'icmp', tmo_icmp]) self.cli_set(base_path + ['netflow', 'timeout', 'max-active-life', tmo_max]) self.cli_set(base_path + ['netflow', 'timeout', 'tcp-fin', tmo_tcp_fin]) self.cli_set(base_path + ['netflow', 'timeout', 'tcp-generic', tmo_tcp_generic]) self.cli_set(base_path + ['netflow', 'timeout', 'tcp-rst', tmo_tcp_rst]) self.cli_set(base_path + ['netflow', 'timeout', 'udp', tmo_udp]) # You need to configure at least one netflow server with self.assertRaises(ConfigSessionError): self.cli_commit() for server, server_config in netflow_server.items(): self.cli_set(base_path + ['netflow', 'server', server]) if 'port' in server_config: self.cli_set(base_path + ['netflow', 'server', server, 'port', server_config['port']]) # commit changes self.cli_commit() uacctd = read_file(uacctd_conf) tmp = [] for server, server_config in netflow_server.items(): tmp_srv = server tmp_srv = tmp_srv.replace('.', '-') tmp_srv = tmp_srv.replace(':', '-') tmp.append(f'nfprobe[nf_{tmp_srv}]') tmp.append('memory') self.assertIn('plugins: ' + ','.join(tmp), uacctd) for server, server_config in netflow_server.items(): tmp_srv = server tmp_srv = tmp_srv.replace('.', '-') tmp_srv = tmp_srv.replace(':', '-') self.assertIn(f'nfprobe_engine[nf_{tmp_srv}]: {engine_id}', uacctd) self.assertIn(f'nfprobe_maxflows[nf_{tmp_srv}]: {max_flows}', uacctd) self.assertIn(f'sampling_rate[nf_{tmp_srv}]: {sampling_rate}', uacctd) self.assertIn(f'nfprobe_source_ip[nf_{tmp_srv}]: {source_address}', uacctd) self.assertIn(f'nfprobe_version[nf_{tmp_srv}]: {version}', uacctd) if 'port' in server_config: self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}', uacctd) else: self.assertIn(f'nfprobe_receiver[nf_{tmp_srv}]: {bracketize_ipv6(server)}:2055', uacctd) self.assertIn(f'nfprobe_timeouts[nf_{tmp_srv}]: expint={tmo_expiry}:general={tmo_flow}:icmp={tmo_icmp}:maxlife={tmo_max}:tcp.fin={tmo_tcp_fin}:tcp={tmo_tcp_generic}:tcp.rst={tmo_tcp_rst}:udp={tmo_udp}', uacctd) self.cli_delete(['interfaces', 'dummy', dummy_if]) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index a0de914bc..21a20ea8d 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -1,206 +1,227 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2023 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 import re from sys import exit from vyos.config import Config -from vyos.firewall import find_nftables_rule -from vyos.firewall import remove_nftables_rule from vyos.utils.process import process_named_running from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args +from vyos.utils.dict import dict_search_recursive from vyos.utils.process import cmd from vyos.utils.process import rc_cmd from vyos.utils.process import run from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() conntrack_config = r'/etc/modprobe.d/vyatta_nf_conntrack.conf' sysctl_file = r'/run/sysctl/10-vyos-conntrack.conf' nftables_ct_file = r'/run/nftables-ct.conf' # Every ALG (Application Layer Gateway) consists of either a Kernel Object # also called a Kernel Module/Driver or some rules present in iptables module_map = { 'ftp' : { 'ko' : ['nf_nat_ftp', 'nf_conntrack_ftp'], }, 'h323' : { 'ko' : ['nf_nat_h323', 'nf_conntrack_h323'], }, 'nfs' : { - 'nftables' : ['ct helper set "rpc_tcp" tcp dport "{111}" return', - 'ct helper set "rpc_udp" udp dport "{111}" return'] + 'nftables' : ['ct helper set "rpc_tcp" tcp dport {111} return', + 'ct helper set "rpc_udp" udp dport {111} return'] }, 'pptp' : { 'ko' : ['nf_nat_pptp', 'nf_conntrack_pptp'], }, 'sip' : { 'ko' : ['nf_nat_sip', 'nf_conntrack_sip'], }, 'sqlnet' : { - 'nftables' : ['ct helper set "tns_tcp" tcp dport "{1521,1525,1536}" return'] + 'nftables' : ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return'] }, 'tftp' : { 'ko' : ['nf_nat_tftp', 'nf_conntrack_tftp'], }, } valid_groups = [ 'address_group', 'domain_group', 'network_group', 'port_group' ] def resync_conntrackd(): tmp = run('/usr/libexec/vyos/conf_mode/conntrack_sync.py') if tmp > 0: print('ERROR: error restarting conntrackd!') def get_config(config=None): if config: conf = config else: conf = Config() base = ['system', 'conntrack'] conntrack = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, with_recursive_defaults=True) - conntrack['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), + conntrack['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + conntrack['flowtable_enabled'] = False + flow_offload = dict_search_args(conntrack['firewall'], 'global_options', 'flow_offload') + if flow_offload and 'disable' not in flow_offload: + for offload_type in ('software', 'hardware'): + if dict_search_args(flow_offload, offload_type, 'interface'): + conntrack['flowtable_enabled'] = True + break + + conntrack['ipv4_nat_action'] = 'accept' if conf.exists(['nat']) else 'return' + conntrack['ipv6_nat_action'] = 'accept' if conf.exists(['nat66']) else 'return' + conntrack['wlb_action'] = 'accept' if conf.exists(['load-balancing', 'wan']) else 'return' + conntrack['wlb_local_action'] = conf.exists(['load-balancing', 'wan', 'enable-local-traffic']) + + conntrack['module_map'] = module_map + return conntrack def verify(conntrack): for inet in ['ipv4', 'ipv6']: if dict_search_args(conntrack, 'ignore', inet, 'rule') != None: for rule, rule_config in conntrack['ignore'][inet]['rule'].items(): if dict_search('destination.port', rule_config) or \ dict_search('destination.group.port_group', rule_config) or \ dict_search('source.port', rule_config) or \ dict_search('source.group.port_group', rule_config): if 'protocol' not in rule_config or rule_config['protocol'] not in ['tcp', 'udp']: raise ConfigError(f'Port requires tcp or udp as protocol in rule {rule}') + tcp_flags = dict_search_args(rule_config, 'tcp', 'flags') + if tcp_flags: + if dict_search_args(rule_config, 'protocol') != 'tcp': + raise ConfigError('Protocol must be tcp when specifying tcp flags') + + not_flags = dict_search_args(rule_config, 'tcp', 'flags', 'not') + if not_flags: + duplicates = [flag for flag in tcp_flags if flag in not_flags] + if duplicates: + raise ConfigError(f'Cannot match a tcp flag as set and not set') + for side in ['destination', 'source']: if side in rule_config: side_conf = rule_config[side] if 'group' in side_conf: if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: raise ConfigError('Only one address-group, network-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: group_name = side_conf['group'][group] error_group = group.replace("_", "-") if group in ['address_group', 'network_group', 'domain_group']: if 'address' in side_conf: raise ConfigError(f'{error_group} and address cannot both be defined') if group_name and group_name[0] == '!': group_name = group_name[1:] if inet == 'ipv6': group = f'ipv6_{group}' - group_obj = dict_search_args(conntrack['firewall_group'], group, group_name) + group_obj = dict_search_args(conntrack['firewall'], 'group', group, group_name) if group_obj is None: raise ConfigError(f'Invalid {error_group} "{group_name}" on ignore rule') if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') return None def generate(conntrack): + if not os.path.exists(nftables_ct_file): + conntrack['first_install'] = True + + # Determine if conntrack is needed + conntrack['ipv4_firewall_action'] = 'return' + conntrack['ipv6_firewall_action'] = 'return' + + if conntrack['flowtable_enabled']: + conntrack['ipv4_firewall_action'] = 'accept' + conntrack['ipv6_firewall_action'] = 'accept' + else: + for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'): + if any(('state' in rule_conf or 'connection_status' in rule_conf) for rule_conf in rules.values()): + if path[0] == 'ipv4': + conntrack['ipv4_firewall_action'] = 'accept' + elif path[0] == 'ipv6': + conntrack['ipv6_firewall_action'] = 'accept' + render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack) render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack) render(nftables_ct_file, 'conntrack/nftables-ct.j2', conntrack) return None -def find_nftables_ct_rule(table, chain, rule): - helper_search = re.search('ct helper set "(\w+)"', rule) - if helper_search: - rule = helper_search[1] - return find_nftables_rule(table, chain, [rule]) - -def find_remove_rule(table, chain, rule): - handle = find_nftables_ct_rule(table, chain, rule) - if handle: - remove_nftables_rule(table, chain, handle) - def apply(conntrack): # Depending on the enable/disable state of the ALG (Application Layer Gateway) # modules we need to either insmod or rmmod the helpers. for module, module_config in module_map.items(): if dict_search(f'modules.{module}', conntrack) is None: if 'ko' in module_config: for mod in module_config['ko']: # Only remove the module if it's loaded if os.path.exists(f'/sys/module/{mod}'): cmd(f'rmmod {mod}') - if 'nftables' in module_config: - for rule in module_config['nftables']: - find_remove_rule('raw', 'VYOS_CT_HELPER', rule) - find_remove_rule('ip6 raw', 'VYOS_CT_HELPER', rule) else: if 'ko' in module_config: for mod in module_config['ko']: cmd(f'modprobe {mod}') - if 'nftables' in module_config: - for rule in module_config['nftables']: - if not find_nftables_ct_rule('raw', 'VYOS_CT_HELPER', rule): - cmd(f'nft insert rule raw VYOS_CT_HELPER {rule}') - - if not find_nftables_ct_rule('ip6 raw', 'VYOS_CT_HELPER', rule): - cmd(f'nft insert rule ip6 raw VYOS_CT_HELPER {rule}') # Load new nftables ruleset install_result, output = rc_cmd(f'nft -f {nftables_ct_file}') if install_result == 1: raise ConfigError(f'Failed to apply configuration: {output}') if process_named_running('conntrackd'): # Reload conntrack-sync daemon to fetch new sysctl values resync_conntrackd() # We silently ignore all errors # See: https://bugzilla.redhat.com/show_bug.cgi?id=1264080 cmd(f'sysctl -f {sysctl_file}') return None 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/conf_mode/firewall.py b/src/conf_mode/firewall.py index 769cc598f..d999b2a64 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -1,422 +1,402 @@ #!/usr/bin/env python3 # # Copyright (C) 2021-2022 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 import re from glob import glob from json import loads from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configdict import node_changed from vyos.configdiff import get_config_diff, Diff from vyos.configdep import set_dependents, call_dependents from vyos.configverify import verify_interface_exists from vyos.firewall import fqdn_config_parse from vyos.firewall import geoip_update from vyos.template import render from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive from vyos.utils.process import process_named_running from vyos.utils.process import rc_cmd from vyos import ConfigError from vyos import airbag airbag.enable() nat_conf_script = 'nat.py' policy_route_conf_script = 'policy-route.py' nftables_conf = '/run/nftables.conf' sysfs_config = { 'all_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_all', 'enable': '0', 'disable': '1'}, 'broadcast_ping': {'sysfs': '/proc/sys/net/ipv4/icmp_echo_ignore_broadcasts', 'enable': '0', 'disable': '1'}, 'ip_src_route': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_source_route'}, 'ipv6_receive_redirects': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_redirects'}, 'ipv6_src_route': {'sysfs': '/proc/sys/net/ipv6/conf/*/accept_source_route', 'enable': '0', 'disable': '-1'}, 'log_martians': {'sysfs': '/proc/sys/net/ipv4/conf/all/log_martians'}, 'receive_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/accept_redirects'}, 'send_redirects': {'sysfs': '/proc/sys/net/ipv4/conf/*/send_redirects'}, 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies'}, 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'} } valid_groups = [ 'address_group', 'domain_group', 'network_group', 'port_group', 'interface_group' ] nested_group_types = [ 'address_group', 'network_group', 'mac_group', 'port_group', 'ipv6_address_group', 'ipv6_network_group' ] snmp_change_type = { 'unknown': 0, 'add': 1, 'delete': 2, 'change': 3 } snmp_event_source = 1 snmp_trap_mib = 'VYATTA-TRAP-MIB' snmp_trap_name = 'mgmtEventTrap' def geoip_updated(conf, firewall): diff = get_config_diff(conf) node_diff = diff.get_child_nodes_diff(['firewall'], expand_nodes=Diff.DELETE, recursive=True) out = { 'name': [], 'ipv6_name': [], 'deleted_name': [], 'deleted_ipv6_name': [] } updated = False for key, path in dict_search_recursive(firewall, 'geoip'): set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' if (path[0] == 'ipv4'): out['name'].append(set_name) elif (path[0] == 'ipv6'): set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}' out['ipv6_name'].append(set_name) updated = True if 'delete' in node_diff: for key, path in dict_search_recursive(node_diff['delete'], 'geoip'): set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' if (path[0] == 'ipv4'): out['deleted_name'].append(set_name) elif (path[0] == 'ipv6'): set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}' out['deleted_ipv6_name'].append(set_name) updated = True if updated: return out return False def get_config(config=None): if config: conf = config else: conf = Config() base = ['firewall'] firewall = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) firewall['group_resync'] = bool('group' in firewall or node_changed(conf, base + ['group'])) if firewall['group_resync']: # Update nat and policy-route as firewall groups were updated set_dependents('group_resync', conf) firewall['geoip_updated'] = geoip_updated(conf, firewall) fqdn_config_parse(firewall) - firewall['flowtable_enabled'] = False - flow_offload = dict_search_args(firewall, 'global_options', 'flow_offload') - if flow_offload and 'disable' not in flow_offload: - for offload_type in ('software', 'hardware'): - if dict_search_args(flow_offload, offload_type, 'interface'): - firewall['flowtable_enabled'] = True - break + set_dependents('conntrack', conf) return firewall def verify_rule(firewall, rule_conf, ipv6): if 'action' not in rule_conf: raise ConfigError('Rule action must be defined') if 'jump' in rule_conf['action'] and 'jump_target' not in rule_conf: raise ConfigError('Action set to jump, but no jump-target specified') if 'jump_target' in rule_conf: if 'jump' not in rule_conf['action']: raise ConfigError('jump-target defined, but action jump needed and it is not defined') target = rule_conf['jump_target'] if not ipv6: if target not in dict_search_args(firewall, 'ipv4', 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') else: if target not in dict_search_args(firewall, 'ipv6', 'name'): raise ConfigError(f'Invalid jump-target. Firewall ipv6 name {target} does not exist on the system') if 'queue_options' in rule_conf: if 'queue' not in rule_conf['action']: raise ConfigError('queue-options defined, but action queue needed and it is not defined') if 'fanout' in rule_conf['queue_options'] and ('queue' not in rule_conf or '-' not in rule_conf['queue']): raise ConfigError('queue-options fanout defined, then queue needs to be defined as a range') if 'queue' in rule_conf and 'queue' not in rule_conf['action']: raise ConfigError('queue defined, but action queue needed and it is not defined') if 'fragment' in rule_conf: if {'match_frag', 'match_non_frag'} <= set(rule_conf['fragment']): raise ConfigError('Cannot specify both "match-frag" and "match-non-frag"') if 'limit' in rule_conf: if 'rate' in rule_conf['limit']: rate_int = re.sub(r'\D', '', rule_conf['limit']['rate']) if int(rate_int) < 1: raise ConfigError('Limit rate integer cannot be less than 1') if 'ipsec' in rule_conf: if {'match_ipsec', 'match_non_ipsec'} <= set(rule_conf['ipsec']): raise ConfigError('Cannot specify both "match-ipsec" and "match-non-ipsec"') if 'recent' in rule_conf: if not {'count', 'time'} <= set(rule_conf['recent']): raise ConfigError('Recent "count" and "time" values must be defined') tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') if tcp_flags: if dict_search_args(rule_conf, 'protocol') != 'tcp': raise ConfigError('Protocol must be tcp when specifying tcp flags') not_flags = dict_search_args(rule_conf, 'tcp', 'flags', 'not') if not_flags: duplicates = [flag for flag in tcp_flags if flag in not_flags] if duplicates: raise ConfigError(f'Cannot match a tcp flag as set and not set') if 'protocol' in rule_conf: if rule_conf['protocol'] == 'icmp' and ipv6: raise ConfigError(f'Cannot match IPv4 ICMP protocol on IPv6, use ipv6-icmp') if rule_conf['protocol'] == 'ipv6-icmp' and not ipv6: raise ConfigError(f'Cannot match IPv6 ICMP protocol on IPv4, use icmp') for side in ['destination', 'source']: if side in rule_conf: side_conf = rule_conf[side] if len({'address', 'fqdn', 'geoip'} & set(side_conf)) > 1: raise ConfigError('Only one of address, fqdn or geoip can be specified') if 'group' in side_conf: if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: raise ConfigError('Only one address-group, network-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: group_name = side_conf['group'][group] fw_group = f'ipv6_{group}' if ipv6 and group in ['address_group', 'network_group'] else group error_group = fw_group.replace("_", "-") if group in ['address_group', 'network_group', 'domain_group']: types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf] if types: raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') if group_name and group_name[0] == '!': group_name = group_name[1:] group_obj = dict_search_args(firewall, 'group', fw_group, group_name) if group_obj is None: raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule') if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') if 'port' in side_conf or dict_search_args(side_conf, 'group', 'port_group'): if 'protocol' not in rule_conf: raise ConfigError('Protocol must be defined if specifying a port or port-group') if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port or port-group') if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'): raise ConfigError(f'{side} port-group and port cannot both be defined') if 'log_options' in rule_conf: if 'log' not in rule_conf or 'enable' not in rule_conf['log']: raise ConfigError('log-options defined, but log is not enable') if 'snapshot_length' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: raise ConfigError('log-options snapshot-length defined, but log group is not define') if 'queue_threshold' in rule_conf['log_options'] and 'group' not in rule_conf['log_options']: raise ConfigError('log-options queue-threshold defined, but log group is not define') for direction in ['inbound_interface','outbound_interface']: if direction in rule_conf: if 'interface_name' in rule_conf[direction] and 'interface_group' in rule_conf[direction]: raise ConfigError(f'Cannot specify both interface-group and interface-name for {direction}') def verify_nested_group(group_name, group, groups, seen): if 'include' not in group: return seen.append(group_name) for g in group['include']: if g not in groups: raise ConfigError(f'Nested group "{g}" does not exist') if g in seen: raise ConfigError(f'Group "{group_name}" has a circular reference') if 'include' in groups[g]: verify_nested_group(g, groups[g], groups, seen) def verify(firewall): if 'group' in firewall: for group_type in nested_group_types: if group_type in firewall['group']: groups = firewall['group'][group_type] for group_name, group in groups.items(): verify_nested_group(group_name, group, groups, []) if 'ipv4' in firewall: for name in ['name','forward','input','output']: if name in firewall['ipv4']: for name_id, name_conf in firewall['ipv4'][name].items(): if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: raise ConfigError('default-action set to jump, but no default-jump-target specified') if 'default_jump_target' in name_conf: target = name_conf['default_jump_target'] if 'jump' not in name_conf['default_action']: raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') if name_conf['default_jump_target'] == name_id: raise ConfigError(f'Loop detected on default-jump-target.') ## Now need to check that default-jump-target exists (other firewall chain/name) if target not in dict_search_args(firewall['ipv4'], 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, False) if 'ipv6' in firewall: for name in ['name','forward','input','output']: if name in firewall['ipv6']: for name_id, name_conf in firewall['ipv6'][name].items(): if 'jump' in name_conf['default_action'] and 'default_jump_target' not in name_conf: raise ConfigError('default-action set to jump, but no default-jump-target specified') if 'default_jump_target' in name_conf: target = name_conf['default_jump_target'] if 'jump' not in name_conf['default_action']: raise ConfigError('default-jump-target defined, but default-action jump needed and it is not defined') if name_conf['default_jump_target'] == name_id: raise ConfigError(f'Loop detected on default-jump-target.') ## Now need to check that default-jump-target exists (other firewall chain/name) if target not in dict_search_args(firewall['ipv6'], 'name'): raise ConfigError(f'Invalid jump-target. Firewall name {target} does not exist on the system') if 'rule' in name_conf: for rule_id, rule_conf in name_conf['rule'].items(): verify_rule(firewall, rule_conf, True) # Verify flow offload options flow_offload = dict_search_args(firewall, 'global_options', 'flow_offload') for offload_type in ('software', 'hardware'): interfaces = dict_search_args(flow_offload, offload_type, 'interface') or [] for interface in interfaces: # nft will raise an error when adding a non-existent interface to a flowtable verify_interface_exists(interface) return None def generate(firewall): if not os.path.exists(nftables_conf): firewall['first_install'] = True - # Determine if conntrack is needed - firewall['ipv4_conntrack_action'] = 'return' - firewall['ipv6_conntrack_action'] = 'return' - if firewall['flowtable_enabled']: # Netfilter's flowtable offload requires conntrack - firewall['ipv4_conntrack_action'] = 'accept' - firewall['ipv6_conntrack_action'] = 'accept' - else: # Check if conntrack is needed by firewall rules - for proto in ('ipv4', 'ipv6'): - for rules, _ in dict_search_recursive(firewall.get(proto, {}), 'rule'): - if any(('state' in rule_conf or 'connection_status' in rule_conf) for rule_conf in rules.values()): - firewall[f'{proto}_conntrack_action'] = 'accept' - break - render(nftables_conf, 'firewall/nftables.j2', firewall) return None def apply_sysfs(firewall): for name, conf in sysfs_config.items(): paths = glob(conf['sysfs']) value = None if name in firewall['global_options']: conf_value = firewall['global_options'][name] if conf_value in conf: value = conf[conf_value] elif conf_value == 'enable': value = '1' elif conf_value == 'disable': value = '0' if value: for path in paths: with open(path, 'w') as f: f.write(value) def apply(firewall): install_result, output = rc_cmd(f'nft -f {nftables_conf}') if install_result == 1: raise ConfigError(f'Failed to apply firewall: {output}') apply_sysfs(firewall) - if firewall['group_resync']: - call_dependents() + call_dependents() # T970 Enable a resolver (systemd daemon) that checks # domain-group/fqdn addresses and update entries for domains by timeout # If router loaded without internet connection or for synchronization domain_action = 'stop' if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'] or firewall['ip6_fqdn']: domain_action = 'restart' call(f'systemctl {domain_action} vyos-domain-resolver.service') if firewall['geoip_updated']: # Call helper script to Update set contents if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']: print('Updating GeoIP. Please wait...') geoip_update(firewall) return None 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/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index 71acd69fa..81ee39df1 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -1,288 +1,288 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2023 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 import re from sys import exit from ipaddress import ip_address from vyos.base import Warning from vyos.config import Config from vyos.config import config_dict_merge from vyos.configverify import verify_vrf from vyos.ifconfig import Section from vyos.template import render from vyos.utils.process import call from vyos.utils.process import cmd from vyos.utils.network import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() uacctd_conf_path = '/run/pmacct/uacctd.conf' systemd_service = 'uacctd.service' systemd_override = f'/run/systemd/system/{systemd_service}.d/override.conf' nftables_nflog_table = 'raw' -nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK' +nftables_nflog_chain = 'VYOS_PREROUTING_HOOK' egress_nftables_nflog_table = 'inet mangle' egress_nftables_nflog_chain = 'FORWARD' # get nftables rule dict for chain in table def _nftables_get_nflog(chain, table): # define list with rules rules = [] # prepare regex for parsing rules rule_pattern = '[io]ifname "(?P<interface>[\w\.\*\-]+)".*handle (?P<handle>[\d]+)' rule_re = re.compile(rule_pattern) # run nftables, save output and split it by lines nftables_command = f'nft -a list chain {table} {chain}' tmp = cmd(nftables_command, message='Failed to get flows list') # parse each line and add information to list for current_rule in tmp.splitlines(): if 'FLOW_ACCOUNTING_RULE' not in current_rule: continue current_rule_parsed = rule_re.search(current_rule) if current_rule_parsed: groups = current_rule_parsed.groupdict() rules.append({ 'interface': groups["interface"], 'table': table, 'handle': groups["handle"] }) # return list with rules return rules def _nftables_config(configured_ifaces, direction, length=None): # define list of nftables commands to modify settings nftable_commands = [] nftables_chain = nftables_nflog_chain nftables_table = nftables_nflog_table if direction == "egress": nftables_chain = egress_nftables_nflog_chain nftables_table = egress_nftables_nflog_table # prepare extended list with configured interfaces configured_ifaces_extended = [] for iface in configured_ifaces: configured_ifaces_extended.append({ 'iface': iface }) # get currently configured interfaces with nftables rules active_nflog_rules = _nftables_get_nflog(nftables_chain, nftables_table) # compare current active list with configured one and delete excessive interfaces, add missed active_nflog_ifaces = [] for rule in active_nflog_rules: interface = rule['interface'] if interface not in configured_ifaces: table = rule['table'] handle = rule['handle'] nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}') else: active_nflog_ifaces.append({ 'iface': interface, }) # do not create new rules for already configured interfaces for iface in active_nflog_ifaces: if iface in active_nflog_ifaces and iface in configured_ifaces_extended: configured_ifaces_extended.remove(iface) # create missed rules for iface_extended in configured_ifaces_extended: iface = iface_extended['iface'] iface_prefix = "o" if direction == "egress" else "i" rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {length} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"' nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}') # Also add IPv6 ingres logging if nftables_table == nftables_nflog_table: nftable_commands.append(f'nft insert rule ip6 {nftables_table} {nftables_chain} {rule_definition}') # change nftables for command in nftable_commands: cmd(command, raising=ConfigError) def get_config(config=None): if config: conf = config else: conf = Config() base = ['system', 'flow-accounting'] if not conf.exists(base): return None flow_accounting = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are # default values which we need to conditionally update into the # dictionary retrieved. default_values = conf.get_config_defaults(**flow_accounting.kwargs, recursive=True) # delete individual flow type defaults - should only be added if user # sets this feature for flow_type in ['sflow', 'netflow']: if flow_type not in flow_accounting and flow_type in default_values: del default_values[flow_type] flow_accounting = config_dict_merge(default_values, flow_accounting) return flow_accounting def verify(flow_config): if not flow_config: return None # check if at least one collector is enabled if 'sflow' not in flow_config and 'netflow' not in flow_config and 'disable_imt' in flow_config: raise ConfigError('You need to configure at least sFlow or NetFlow, ' \ 'or not set "disable-imt" for flow-accounting!') # Check if at least one interface is configured if 'interface' not in flow_config: raise ConfigError('Flow accounting requires at least one interface to ' \ 'be configured!') # check that all configured interfaces exists in the system for interface in flow_config['interface']: if interface not in Section.interfaces(): # Changed from error to warning to allow adding dynamic interfaces # and interface templates Warning(f'Interface "{interface}" is not presented in the system') # check sFlow configuration if 'sflow' in flow_config: # check if at least one sFlow collector is configured if 'server' not in flow_config['sflow']: raise ConfigError('You need to configure at least one sFlow server!') # check that all sFlow collectors use the same IP protocol version sflow_collector_ipver = None for server in flow_config['sflow']['server']: if sflow_collector_ipver: if sflow_collector_ipver != ip_address(server).version: raise ConfigError("All sFlow servers must use the same IP protocol") else: sflow_collector_ipver = ip_address(server).version # check if vrf is defined for Sflow verify_vrf(flow_config) sflow_vrf = None if 'vrf' in flow_config: sflow_vrf = flow_config['vrf'] # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa for server in flow_config['sflow']['server']: if 'agent_address' in flow_config['sflow']: if ip_address(server).version != ip_address(flow_config['sflow']['agent_address']).version: raise ConfigError('IPv4 and IPv6 addresses can not be mixed in "sflow agent-address" and "sflow '\ 'server". You need to set the same IP version for both "agent-address" and '\ 'all sFlow servers') if 'agent_address' in flow_config['sflow']: tmp = flow_config['sflow']['agent_address'] if not is_addr_assigned(tmp, sflow_vrf): raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!') # Check if configured sflow source-address exist in the system if 'source_address' in flow_config['sflow']: if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf): tmp = flow_config['sflow']['source_address'] raise ConfigError(f'Configured "sflow source-address {tmp}" does not exist on the system!') # check NetFlow configuration if 'netflow' in flow_config: # check if vrf is defined for netflow netflow_vrf = None if 'vrf' in flow_config: netflow_vrf = flow_config['vrf'] # check if at least one NetFlow collector is configured if NetFlow configuration is presented if 'server' not in flow_config['netflow']: raise ConfigError('You need to configure at least one NetFlow server!') # Check if configured netflow source-address exist in the system if 'source_address' in flow_config['netflow']: if not is_addr_assigned(flow_config['netflow']['source_address'], netflow_vrf): tmp = flow_config['netflow']['source_address'] raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!') # Check if engine-id compatible with selected protocol version if 'engine_id' in flow_config['netflow']: v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$' v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$' engine_id = flow_config['netflow']['engine_id'] version = flow_config['netflow']['version'] if flow_config['netflow']['version'] == '5': regex_filter = re.compile(v5_filter) if not regex_filter.search(engine_id): raise ConfigError(f'You cannot use NetFlow engine-id "{engine_id}" '\ f'together with NetFlow protocol version "{version}"!') else: regex_filter = re.compile(v9v10_filter) if not regex_filter.search(flow_config['netflow']['engine_id']): raise ConfigError(f'Can not use NetFlow engine-id "{engine_id}" together '\ f'with NetFlow protocol version "{version}"!') # return True if all checks were passed return True def generate(flow_config): if not flow_config: return None render(uacctd_conf_path, 'pmacct/uacctd.conf.j2', flow_config) render(systemd_override, 'pmacct/override.conf.j2', flow_config) # Reload systemd manager configuration call('systemctl daemon-reload') def apply(flow_config): action = 'restart' # Check if flow-accounting was removed and define command if not flow_config: _nftables_config([], 'ingress') _nftables_config([], 'egress') # Stop flow-accounting daemon and remove configuration file call(f'systemctl stop {systemd_service}') if os.path.exists(uacctd_conf_path): os.unlink(uacctd_conf_path) return # Start/reload flow-accounting daemon call(f'systemctl restart {systemd_service}') # configure nftables rules for defined interfaces if 'interface' in flow_config: _nftables_config(flow_config['interface'], 'ingress', flow_config['packet_length']) # configure egress the same way if configured otherwise remove it if 'enable_egress' in flow_config: _nftables_config(flow_config['interface'], 'egress', flow_config['packet_length']) else: _nftables_config([], 'egress') if __name__ == '__main__': try: config = get_config() verify(config) generate(config) apply(config) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/load-balancing-wan.py b/src/conf_mode/load-balancing-wan.py index ad9c80d72..5da0b906b 100755 --- a/src/conf_mode/load-balancing-wan.py +++ b/src/conf_mode/load-balancing-wan.py @@ -1,146 +1,151 @@ #!/usr/bin/env python3 # # Copyright (C) 2023 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 from shutil import rmtree from vyos.base import Warning from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.utils.process import cmd from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() load_balancing_dir = '/run/load-balance' load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf' systemd_service = 'vyos-wan-load-balance.service' def get_config(config=None): if config: conf = config else: conf = Config() base = ['load-balancing', 'wan'] lb = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, with_recursive_defaults=True) # prune limit key if not set by user for rule in lb.get('rule', []): if lb.from_defaults(['rule', rule, 'limit']): del lb['rule'][rule]['limit'] + set_dependents('conntrack', conf) + return lb def verify(lb): if not lb: return None if 'interface_health' not in lb: raise ConfigError( 'A valid WAN load-balance configuration requires an interface with a nexthop!' ) for interface, interface_config in lb['interface_health'].items(): if 'nexthop' not in interface_config: raise ConfigError( f'interface-health {interface} nexthop must be specified!') if 'test' in interface_config: for test_rule, test_config in interface_config['test'].items(): if 'type' in test_config: if test_config['type'] == 'user-defined' and 'test_script' not in test_config: raise ConfigError( f'test {test_rule} script must be defined for test-script!' ) if 'rule' not in lb: Warning( 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!' ) else: for rule, rule_config in lb['rule'].items(): if 'inbound_interface' not in rule_config: raise ConfigError(f'rule {rule} inbound-interface must be specified!') if {'failover', 'exclude'} <= set(rule_config): raise ConfigError(f'rule {rule} failover cannot be configured with exclude!') if {'limit', 'exclude'} <= set(rule_config): raise ConfigError(f'rule {rule} limit cannot be used with exclude!') if 'interface' not in rule_config: if 'exclude' not in rule_config: Warning( f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule' ) for direction in {'source', 'destination'}: if direction in rule_config: if 'protocol' in rule_config and 'port' in rule_config[ direction]: if rule_config['protocol'] not in {'tcp', 'udp'}: raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"') def generate(lb): if not lb: # Delete /run/load-balance/wlb.conf if os.path.isfile(load_balancing_conf_file): os.unlink(load_balancing_conf_file) # Delete old directories if os.path.isdir(load_balancing_dir): rmtree(load_balancing_dir, ignore_errors=True) if os.path.exists('/var/run/load-balance/wlb.out'): os.unlink('/var/run/load-balance/wlb.out') return None # Create load-balance dir if not os.path.isdir(load_balancing_dir): os.mkdir(load_balancing_dir) render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb) return None def apply(lb): if not lb: try: cmd(f'systemctl stop {systemd_service}') except Exception as e: print(f"Error message: {e}") else: cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1') cmd(f'systemctl restart {systemd_service}') + call_dependents() + return None 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/conf_mode/nat.py b/src/conf_mode/nat.py index e37a7011c..52a7a71fd 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,281 +1,237 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-2023 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 jmespath import json import os -from distutils.version import LooseVersion -from platform import release as kernel_version from sys import exit from netifaces import interfaces from vyos.base import Warning from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.template import render from vyos.template import is_ip_network from vyos.utils.kernel import check_kmod from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.process import cmd from vyos.utils.process import run from vyos.utils.network import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() -if LooseVersion(kernel_version()) > LooseVersion('5.1'): - k_mod = ['nft_nat', 'nft_chain_nat'] -else: - k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] +k_mod = ['nft_nat', 'nft_chain_nat'] nftables_nat_config = '/run/nftables_nat.conf' nftables_static_nat_conf = '/run/nftables_static-nat-rules.nft' valid_groups = [ 'address_group', 'domain_group', 'network_group', 'port_group' ] -def get_handler(json, chain, target): - """ Get nftable rule handler number of given chain/target combination. - Handler is required when adding NAT/Conntrack helper targets """ - for x in json: - if x['chain'] != chain: - continue - if x['target'] != target: - continue - return x['handle'] +def get_config(config=None): + if config: + conf = config + else: + conf = Config() - return None + base = ['nat'] + nat = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, + with_recursive_defaults=True) + set_dependents('conntrack', conf) + + if not conf.exists(base): + nat['deleted'] = '' + return nat + + nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, + no_tag_node_value_mangle=True) + + return nat def verify_rule(config, err_msg, groups_dict): """ Common verify steps used for both source and destination NAT """ if (dict_search('translation.port', config) != None or dict_search('translation.redirect.port', config) != None or dict_search('destination.port', config) != None or dict_search('source.port', config)): if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError(f'{err_msg}\n' \ 'ports can only be specified when protocol is '\ 'either tcp, udp or tcp_udp!') if is_ip_network(dict_search('translation.address', config)): raise ConfigError(f'{err_msg}\n' \ 'Cannot use ports with an IPv4 network as translation address as it\n' \ 'statically maps a whole network of addresses onto another\n' \ 'network of addresses') for side in ['destination', 'source']: if side in config: side_conf = config[side] if len({'address', 'fqdn'} & set(side_conf)) > 1: raise ConfigError('Only one of address, fqdn or geoip can be specified') if 'group' in side_conf: if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1: raise ConfigError('Only one address-group, network-group or domain-group can be specified') for group in valid_groups: if group in side_conf['group']: group_name = side_conf['group'][group] error_group = group.replace("_", "-") if group in ['address_group', 'network_group', 'domain_group']: types = [t for t in ['address', 'fqdn'] if t in side_conf] if types: raise ConfigError(f'{error_group} and {types[0]} cannot both be defined') if group_name and group_name[0] == '!': group_name = group_name[1:] group_obj = dict_search_args(groups_dict, group, group_name) if group_obj is None: raise ConfigError(f'Invalid {error_group} "{group_name}" on nat rule') if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') if dict_search_args(side_conf, 'group', 'port_group'): if 'protocol' not in config: raise ConfigError('Protocol must be defined if specifying a port-group') if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']: raise ConfigError('Protocol must be tcp, udp, or tcp_udp when specifying a port-group') if 'load_balance' in config: for item in ['source-port', 'destination-port']: if item in config['load_balance']['hash'] and config['protocol'] not in ['tcp', 'udp']: raise ConfigError('Protocol must be tcp or udp when specifying hash ports') count = 0 if 'backend' in config['load_balance']: for member in config['load_balance']['backend']: weight = config['load_balance']['backend'][member]['weight'] count = count + int(weight) if count != 100: Warning(f'Sum of weight for nat load balance rule is not 100. You may get unexpected behaviour') -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base = ['nat'] - nat = conf.get_config_dict(base, key_mangling=('-', '_'), - get_first_key=True, - with_recursive_defaults=True) - - # read in current nftable (once) for further processing - tmp = cmd('nft -j list table raw') - nftable_json = json.loads(tmp) - - # condense the full JSON table into a list with only relevand informations - pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}' - condensed_json = jmespath.search(pattern, nftable_json) - - if not conf.exists(base): - if get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER'): - nat['helper_functions'] = 'remove' - - # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') - nat['deleted'] = '' - return nat - - nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True, - no_tag_node_value_mangle=True) - - # check if NAT connection tracking helpers need to be set up - this has to - # be done only once - if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): - nat['helper_functions'] = 'add' - - # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_IGNORE') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_PREROUTING_HOOK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_IGNORE') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_OUTPUT_HOOK') - - return nat - def verify(nat): if not nat or 'deleted' in nat: # no need to verify the CLI as NAT is going to be deactivated return None - if 'helper_functions' in nat: - if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']): - raise Exception('could not determine nftable ruleset handlers') - if dict_search('source.rule', nat): for rule, config in dict_search('source.rule', nat).items(): err_msg = f'Source NAT configuration error in rule {rule}:' if 'outbound_interface' in config: if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') if not dict_search('translation.address', config) and not dict_search('translation.port', config): if 'exclude' not in config and 'backend' not in config['load_balance']: raise ConfigError(f'{err_msg} translation requires address and/or port') addr = dict_search('translation.address', config) if addr != None and addr != 'masquerade' and not is_ip_network(addr): for ip in addr.split('-'): if not is_addr_assigned(ip): Warning(f'IP address {ip} does not exist on the system!') # common rule verification verify_rule(config, err_msg, nat['firewall_group']) if dict_search('destination.rule', nat): for rule, config in dict_search('destination.rule', nat).items(): err_msg = f'Destination NAT configuration error in rule {rule}:' if 'inbound_interface' in config: if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') if not dict_search('translation.address', config) and not dict_search('translation.port', config) and 'redirect' not in config['translation']: if 'exclude' not in config and 'backend' not in config['load_balance']: raise ConfigError(f'{err_msg} translation requires address and/or port') # common rule verification verify_rule(config, err_msg, nat['firewall_group']) 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, nat['firewall_group']) return None def generate(nat): if not os.path.exists(nftables_nat_config): nat['first_install'] = True 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: raise ConfigError('Configuration file errors encountered!') tmp = run(f'nft -c -f {nftables_static_nat_conf}') if tmp > 0: raise ConfigError('Configuration file errors encountered!') return None def apply(nat): cmd(f'nft -f {nftables_nat_config}') cmd(f'nft -f {nftables_static_nat_conf}') if not nat or 'deleted' in nat: os.unlink(nftables_nat_config) os.unlink(nftables_static_nat_conf) + call_dependents() + return None if __name__ == '__main__': try: check_kmod(k_mod) c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py index 4c12618bc..46d796bc8 100755 --- a/src/conf_mode/nat66.py +++ b/src/conf_mode/nat66.py @@ -1,169 +1,131 @@ #!/usr/bin/env python3 # # Copyright (C) 2020-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 jmespath import json import os from sys import exit from netifaces import interfaces from vyos.base import Warning from vyos.config import Config +from vyos.configdep import set_dependents, call_dependents from vyos.template import render from vyos.utils.process import cmd from vyos.utils.kernel import check_kmod from vyos.utils.dict import dict_search from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag airbag.enable() k_mod = ['nft_nat', 'nft_chain_nat'] nftables_nat66_config = '/run/nftables_nat66.nft' ndppd_config = '/run/ndppd/ndppd.conf' -def get_handler(json, chain, target): - """ Get nftable rule handler number of given chain/target combination. - Handler is required when adding NAT66/Conntrack helper targets """ - for x in json: - if x['chain'] != chain: - continue - if x['target'] != target: - continue - return x['handle'] - - return None - def get_config(config=None): if config: conf = config else: conf = Config() base = ['nat66'] nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) - # read in current nftable (once) for further processing - tmp = cmd('nft -j list table ip6 raw') - nftable_json = json.loads(tmp) - - # condense the full JSON table into a list with only relevand informations - pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}' - condensed_json = jmespath.search(pattern, nftable_json) + set_dependents('conntrack', conf) if not conf.exists(base): - nat['helper_functions'] = 'remove' - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_HELPER') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_HELPER') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK') nat['deleted'] = '' - return nat - - # check if NAT66 connection tracking helpers need to be set up - this has to - # be done only once - if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'): - nat['helper_functions'] = 'add' - - # Retrieve current table handler positions - nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_IGNORE') - nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYOS_CT_PREROUTING_HOOK') - nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_IGNORE') - nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYOS_CT_OUTPUT_HOOK') - else: - nat['helper_functions'] = 'has' return nat def verify(nat): if not nat or 'deleted' in nat: # no need to verify the CLI as NAT66 is going to be deactivated return None - if 'helper_functions' in nat and nat['helper_functions'] != 'has': - if not (nat['pre_ct_conntrack'] or nat['out_ct_conntrack']): - raise Exception('could not determine nftable ruleset handlers') - if dict_search('source.rule', nat): for rule, config in dict_search('source.rule', nat).items(): err_msg = f'Source NAT66 configuration error in rule {rule}:' if 'outbound_interface' not in config: raise ConfigError(f'{err_msg} outbound-interface not specified') if config['outbound_interface'] not in interfaces(): raise ConfigError(f'rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system') addr = dict_search('translation.address', config) if addr != None: if addr != 'masquerade' and not is_ipv6(addr): raise ConfigError(f'IPv6 address {addr} is not a valid address') else: if 'exclude' not in config: raise ConfigError(f'{err_msg} translation address not specified') prefix = dict_search('source.prefix', config) if prefix != None: if not is_ipv6(prefix): raise ConfigError(f'{err_msg} source-prefix not specified') if dict_search('destination.rule', nat): for rule, config in dict_search('destination.rule', nat).items(): err_msg = f'Destination NAT66 configuration error in rule {rule}:' if 'inbound_interface' not in config: raise ConfigError(f'{err_msg}\n' \ 'inbound-interface not specified') else: if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces(): Warning(f'rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system') return None def generate(nat): if not os.path.exists(nftables_nat66_config): nat['first_install'] = True render(nftables_nat66_config, 'firewall/nftables-nat66.j2', nat, permission=0o755) render(ndppd_config, 'ndppd/ndppd.conf.j2', nat, permission=0o755) return None def apply(nat): if not nat: return None cmd(f'nft -f {nftables_nat66_config}') if 'deleted' in nat or not dict_search('source.rule', nat): cmd('systemctl stop ndppd') if os.path.isfile(ndppd_config): os.unlink(ndppd_config) else: cmd('systemctl restart ndppd') + call_dependents() + return None if __name__ == '__main__': try: check_kmod(k_mod) c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1)