From fdeba8da3e99256fe449e331d0b833a941315226 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Wed, 28 Jul 2021 12:03:21 +0200
Subject: firewall: T2199: Migrate firewall to XML/Python
---
src/conf_mode/conntrack.py | 39 +++--
src/conf_mode/firewall-interface.py | 146 +++++++++++++++++++
src/conf_mode/firewall.py | 267 ++++++++++++++++++++++++++++++++--
src/conf_mode/flow_accounting_conf.py | 89 ++++++------
src/conf_mode/nat.py | 13 +-
src/conf_mode/nat66.py | 12 +-
src/conf_mode/protocols_nhrp.py | 27 ++--
src/migration-scripts/firewall/6-to-7 | 72 +++++++++
src/validators/ip-protocol | 2 +-
9 files changed, 565 insertions(+), 102 deletions(-)
create mode 100755 src/conf_mode/firewall-interface.py
create mode 100755 src/migration-scripts/firewall/6-to-7
(limited to 'src')
diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py
index 68877f794..c65ef9540 100755
--- a/src/conf_mode/conntrack.py
+++ b/src/conf_mode/conntrack.py
@@ -15,11 +15,14 @@
# along with this program. If not, see .
import os
+import re
from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
+from vyos.firewall import find_nftables_rule
+from vyos.firewall import remove_nftables_rule
from vyos.util import cmd
from vyos.util import run
from vyos.util import process_named_running
@@ -43,8 +46,8 @@ module_map = {
'ko' : ['nf_nat_h323', 'nf_conntrack_h323'],
},
'nfs' : {
- 'iptables' : ['VYATTA_CT_HELPER --table raw --proto tcp --dport 111 --jump CT --helper rpc',
- 'VYATTA_CT_HELPER --table raw --proto udp --dport 111 --jump CT --helper rpc'],
+ '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'],
@@ -53,9 +56,7 @@ module_map = {
'ko' : ['nf_nat_sip', 'nf_conntrack_sip'],
},
'sqlnet' : {
- 'iptables' : ['VYATTA_CT_HELPER --table raw --proto tcp --dport 1521 --jump CT --helper tns',
- 'VYATTA_CT_HELPER --table raw --proto tcp --dport 1525 --jump CT --helper tns',
- 'VYATTA_CT_HELPER --table raw --proto tcp --dport 1536 --jump CT --helper tns'],
+ 'nftables' : ['ct helper set "tns_tcp" tcp dport "{1521,1525,1536}" return']
},
'tftp' : {
'ko' : ['nf_nat_tftp', 'nf_conntrack_tftp'],
@@ -93,6 +94,17 @@ def generate(conntrack):
return None
+def find_nftables_ct_rule(rule):
+ helper_search = re.search('ct helper set "(\w+)"', rule)
+ if helper_search:
+ rule = helper_search[1]
+ return find_nftables_rule('raw', 'VYOS_CT_HELPER', [rule])
+
+def find_remove_rule(rule):
+ handle = find_nftables_ct_rule(rule)
+ if handle:
+ remove_nftables_rule('raw', 'VYOS_CT_HELPER', 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.
@@ -103,20 +115,17 @@ def apply(conntrack):
# Only remove the module if it's loaded
if os.path.exists(f'/sys/module/{mod}'):
cmd(f'rmmod {mod}')
- if 'iptables' in module_config:
- for rule in module_config['iptables']:
- # Only install iptables rule if it does not exist
- tmp = run(f'iptables --check {rule}')
- if tmp == 0: cmd(f'iptables --delete {rule}')
+ if 'nftables' in module_config:
+ for rule in module_config['nftables']:
+ find_remove_rule(rule)
else:
if 'ko' in module_config:
for mod in module_config['ko']:
cmd(f'modprobe {mod}')
- if 'iptables' in module_config:
- for rule in module_config['iptables']:
- # Only install iptables rule if it does not exist
- tmp = run(f'iptables --check {rule}')
- if tmp > 0: cmd(f'iptables --insert {rule}')
+ if 'nftables' in module_config:
+ for rule in module_config['nftables']:
+ if not find_nftables_ct_rule(rule):
+ cmd(f'nft insert rule ip raw VYOS_CT_HELPER {rule}')
if process_named_running('conntrackd'):
# Reload conntrack-sync daemon to fetch new sysctl values
diff --git a/src/conf_mode/firewall-interface.py b/src/conf_mode/firewall-interface.py
new file mode 100755
index 000000000..3a17dc5a4
--- /dev/null
+++ b/src/conf_mode/firewall-interface.py
@@ -0,0 +1,146 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+import re
+
+from sys import argv
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import leaf_node_changed
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.util import cmd
+from vyos.util import dict_search_args
+from vyos.util import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+NFT_CHAINS = {
+ 'in': 'VYOS_FW_IN',
+ 'out': 'VYOS_FW_OUT',
+ 'local': 'VYOS_FW_LOCAL'
+}
+NFT6_CHAINS = {
+ 'in': 'VYOS_FW6_IN',
+ 'out': 'VYOS_FW6_OUT',
+ 'local': 'VYOS_FW6_LOCAL'
+}
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ ifname = argv[1]
+ ifpath = Section.get_config_path(ifname)
+ if_firewall_path = f'interfaces {ifpath} firewall'
+
+ if_firewall = conf.get_config_dict(if_firewall_path, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ if_firewall['ifname'] = ifname
+ if_firewall['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return if_firewall
+
+def verify(if_firewall):
+ # bail out early - looks like removal from running config
+ if not if_firewall:
+ return None
+
+ for direction in ['in', 'out', 'local']:
+ if direction in if_firewall:
+ if 'name' in if_firewall[direction]:
+ name = if_firewall[direction]['name']
+
+ if 'name' not in if_firewall['firewall']:
+ raise ConfigError('Firewall name not configured')
+
+ if name not in if_firewall['firewall']['name']:
+ raise ConfigError(f'Invalid firewall name "{name}"')
+
+ if 'ipv6_name' in if_firewall[direction]:
+ name = if_firewall[direction]['ipv6_name']
+
+ if 'ipv6_name' not in if_firewall['firewall']:
+ raise ConfigError('Firewall ipv6-name not configured')
+
+ if name not in if_firewall['firewall']['ipv6_name']:
+ raise ConfigError(f'Invalid firewall ipv6-name "{name}"')
+
+ return None
+
+def generate(if_firewall):
+ return None
+
+def cleanup_rule(table, chain, ifname, new_name=None):
+ results = cmd(f'nft -a list chain {table} {chain}').split("\n")
+ retval = None
+ for line in results:
+ if f'ifname "{ifname}"' in line:
+ if new_name and f'jump {new_name}' in line:
+ # new_name is used to clear rules for any previously referenced chains
+ # returns true when rule exists and doesn't need to be created
+ retval = True
+ continue
+
+ handle_search = re.search('handle (\d+)', line)
+ if handle_search:
+ run(f'nft delete rule {table} {chain} handle {handle_search[1]}')
+ return retval
+
+def apply(if_firewall):
+ ifname = if_firewall['ifname']
+
+ for direction in ['in', 'out', 'local']:
+ chain = NFT_CHAINS[direction]
+ ipv6_chain = NFT6_CHAINS[direction]
+ if_prefix = 'i' if direction in ['in', 'local'] else 'o'
+
+ name = dict_search_args(if_firewall, direction, 'name')
+ if name:
+ rule_exists = cleanup_rule('ip filter', chain, ifname, name)
+
+ if not rule_exists:
+ run(f'nft insert rule ip filter {chain} {if_prefix}ifname {ifname} counter jump {name}')
+ else:
+ cleanup_rule('ip filter', chain, ifname)
+
+ ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
+ if ipv6_name:
+ rule_exists = cleanup_rule('ip6 filter', ipv6_chain, ifname, ipv6_name)
+
+ if not rule_exists:
+ run(f'nft insert rule ip6 filter {ipv6_chain} {if_prefix}ifname {ifname} counter jump {ipv6_name}')
+ else:
+ cleanup_rule('ip6 filter', ipv6_chain, ifname)
+
+ 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 8e6ce5b14..5ac48c9ba 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -16,50 +16,295 @@
import os
+from glob import glob
+from json import loads
from sys import exit
from vyos.config import Config
from vyos.configdict import dict_merge
-from vyos.configdict import node_changed
-from vyos.configdict import leaf_node_changed
+from vyos.configdiff import get_config_diff, Diff
from vyos.template import render
-from vyos.util import call
+from vyos.util import cmd
+from vyos.util import dict_search_args
+from vyos.util import process_named_running
+from vyos.util import run
+from vyos.xml import defaults
from vyos import ConfigError
from vyos import airbag
-from pprint import pprint
airbag.enable()
+nftables_conf = '/run/nftables.conf'
-def get_config(config=None):
+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'},
+ 'source_validation': {'sysfs': '/proc/sys/net/ipv4/conf/*/rp_filter', 'disable': '0', 'strict': '1', 'loose': '2'},
+ 'syn_cookies': {'sysfs': '/proc/sys/net/ipv4/tcp_syncookies'},
+ 'twa_hazards_protection': {'sysfs': '/proc/sys/net/ipv4/tcp_rfc1337'}
+}
+
+preserve_chains = [
+ 'INPUT',
+ 'FORWARD',
+ 'OUTPUT',
+ 'VYOS_FW_IN',
+ 'VYOS_FW_OUT',
+ 'VYOS_FW_LOCAL',
+ 'VYOS_FW_OUTPUT',
+ 'VYOS_POST_FW',
+ 'VYOS_FRAG_MARK',
+ 'VYOS_FW6_IN',
+ 'VYOS_FW6_OUT',
+ 'VYOS_FW6_LOCAL',
+ 'VYOS_FW6_OUTPUT',
+ 'VYOS_POST_FW6',
+ 'VYOS_FRAG6_MARK'
+]
+
+valid_groups = [
+ 'address_group',
+ 'network_group',
+ 'port_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 get_firewall_interfaces(conf):
+ out = {}
+ interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+ def find_interfaces(iftype_conf, output={}, prefix=''):
+ for ifname, if_conf in iftype_conf.items():
+ if 'firewall' in if_conf:
+ output[prefix + ifname] = if_conf['firewall']
+ for vif in ['vif', 'vif_s', 'vif_c']:
+ if vif in if_conf:
+ output.update(find_interfaces(if_conf[vif], output, f'{prefix}{ifname}.'))
+ return output
+ for iftype, iftype_conf in interfaces.items():
+ out.update(find_interfaces(iftype_conf))
+ return out
+
+def get_config(config=None):
if config:
conf = config
else:
conf = Config()
- base = ['nfirewall']
+ base = ['firewall']
+
+ if not conf.exists(base):
+ return {}
+
firewall = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
- pprint(firewall)
+ default_values = defaults(base)
+ firewall = dict_merge(default_values, firewall)
+
+ firewall['interfaces'] = get_firewall_interfaces(conf)
+
+ if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
+ diff = get_config_diff(conf)
+ firewall['trap_diff'] = diff.get_child_nodes_diff_str(base)
+ firewall['trap_targets'] = conf.get_config_dict(['service', 'snmp', 'trap-target'],
+ key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
return firewall
+def verify_rule(firewall, rule_conf, ipv6):
+ if 'action' not in rule_conf:
+ raise ConfigError('Rule action must be 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 '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')
+
+ for side in ['destination', 'source']:
+ if side in rule_conf:
+ side_conf = rule_conf[side]
+
+ if 'group' in side_conf:
+ if {'address_group', 'network_group'} <= set(side_conf['group']):
+ raise ConfigError('Only one address-group or network-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
+
+ if not dict_search_args(firewall, 'group', fw_group):
+ error_group = fw_group.replace("_", "-")
+ raise ConfigError(f'Group defined in rule but {error_group} is not configured')
+
+ if group_name not in firewall['group'][fw_group]:
+ error_group = group.replace("_", "-")
+ raise ConfigError(f'Invalid {error_group} "{group_name}" on firewall rule')
+
+ 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')
+
def verify(firewall):
# bail out early - looks like removal from running config
if not firewall:
return None
+ if 'config_trap' in firewall and firewall['config_trap'] == 'enable':
+ if not firewall['trap_targets']:
+ raise ConfigError(f'Firewall config-trap enabled but "service snmp trap-target" is not defined')
+
+ for name in ['name', 'ipv6_name']:
+ if name in firewall:
+ for name_id, name_conf in firewall[name].items():
+ if name_id in preserve_chains:
+ raise ConfigError(f'Firewall name "{name_id}" is reserved for VyOS')
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ verify_rule(firewall, rule_conf, name == 'ipv6_name')
+
+ for ifname, if_firewall in firewall['interfaces'].items():
+ for direction in ['in', 'out', 'local']:
+ name = dict_search_args(if_firewall, direction, 'name')
+ ipv6_name = dict_search_args(if_firewall, direction, 'ipv6_name')
+
+ if name and not dict_search_args(firewall, 'name', name):
+ raise ConfigError(f'Firewall name "{name}" is still referenced on interface {ifname}')
+
+ if ipv6_name and not dict_search_args(firewall, 'ipv6_name', ipv6_name):
+ raise ConfigError(f'Firewall ipv6-name "{ipv6_name}" is still referenced on interface {ifname}')
+
return None
+def cleanup_commands(firewall):
+ commands = []
+ for table in ['ip filter', 'ip6 filter']:
+ json_str = cmd(f'nft -j list table {table}')
+ obj = loads(json_str)
+ if 'nftables' not in obj:
+ continue
+ for item in obj['nftables']:
+ if 'chain' in item:
+ if item['chain']['name'] not in preserve_chains:
+ chain = item['chain']['name']
+ if table == 'ip filter' and dict_search_args(firewall, 'name', chain):
+ commands.append(f'flush chain {table} {chain}')
+ elif table == 'ip6 filter' and dict_search_args(firewall, 'ipv6_name', chain):
+ commands.append(f'flush chain {table} {chain}')
+ else:
+ commands.append(f'delete chain {table} {chain}')
+ return commands
+
def generate(firewall):
- if not firewall:
- return None
+ if not os.path.exists(nftables_conf):
+ firewall['first_install'] = True
+ else:
+ firewall['cleanup_commands'] = cleanup_commands(firewall)
+ render(nftables_conf, 'firewall/nftables.tmpl', firewall)
return None
-def apply(firewall):
- if not firewall:
+def apply_sysfs(firewall):
+ for name, conf in sysfs_config.items():
+ paths = glob(conf['sysfs'])
+ value = None
+
+ if name in firewall:
+ conf_value = firewall[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 post_apply_trap(firewall):
+ if 'first_install' in firewall:
return None
+ if 'config_trap' not in firewall or firewall['config_trap'] != 'enable':
+ return None
+
+ if not process_named_running('snmpd'):
+ return None
+
+ trap_username = os.getlogin()
+
+ for host, target_conf in firewall['trap_targets'].items():
+ community = target_conf['community'] if 'community' in target_conf else 'public'
+ port = int(target_conf['port']) if 'port' in target_conf else 162
+
+ base_cmd = f'snmptrap -v2c -c {community} {host}:{port} 0 {snmp_trap_mib}::{snmp_trap_name} '
+
+ for change_type, changes in firewall['trap_diff'].items():
+ for path_str, value in changes.items():
+ objects = [
+ f'mgmtEventUser s "{trap_username}"',
+ f'mgmtEventSource i {snmp_event_source}',
+ f'mgmtEventType i {snmp_change_type[change_type]}'
+ ]
+
+ if change_type == 'add':
+ objects.append(f'mgmtEventCurrCfg s "{path_str} {value}"')
+ elif change_type == 'delete':
+ objects.append(f'mgmtEventPrevCfg s "{path_str} {value}"')
+ elif change_type == 'change':
+ objects.append(f'mgmtEventPrevCfg s "{path_str} {value[0]}"')
+ objects.append(f'mgmtEventCurrCfg s "{path_str} {value[1]}"')
+
+ cmd(base_cmd + ' '.join(objects))
+
+def apply(firewall):
+ if 'first_install' in firewall:
+ run('nfct helper add rpc inet tcp')
+ run('nfct helper add rpc inet udp')
+ run('nfct helper add tns inet tcp')
+
+ install_result = run(f'nft -f {nftables_conf}')
+ if install_result == 1:
+ raise ConfigError('Failed to apply firewall')
+
+ if 'state_policy' in firewall:
+ for chain in ['INPUT', 'OUTPUT', 'FORWARD']:
+ cmd(f'nft insert rule ip filter {chain} jump VYOS_STATE_POLICY')
+ cmd(f'nft insert rule ip6 filter {chain} jump VYOS_STATE_POLICY6')
+
+ apply_sysfs(firewall)
+
+ post_apply_trap(firewall)
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 0a4559ade..1b036a53f 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -40,10 +40,10 @@ default_captured_packet_size = 128
default_netflow_version = '9'
default_sflow_agentip = 'auto'
uacctd_conf_path = '/etc/pmacct/uacctd.conf'
-iptables_nflog_table = 'raw'
-iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK'
-egress_iptables_nflog_table = 'mangle'
-egress_iptables_nflog_chain = 'FORWARD'
+nftables_nflog_table = 'raw'
+nftables_nflog_chain = 'VYOS_CT_PREROUTING_HOOK'
+egress_nftables_nflog_table = 'inet mangle'
+egress_nftables_nflog_chain = 'FORWARD'
# helper functions
# check if node exists and return True if this is true
@@ -75,62 +75,61 @@ def _sflow_default_agentip(config):
# return nothing by default
return None
-# get iptables rule dict for chain in table
-def _iptables_get_nflog(chain, table):
+# 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 = "^-A (?P{0} (\-i|\-o) (?P[\w\.\*\-]+).*--comment FLOW_ACCOUNTING_RULE.* -j NFLOG.*$)".format(chain)
+ rule_pattern = '[io]ifname "(?P[\w\.\*\-]+)".*handle (?P[\d]+)'
rule_re = re.compile(rule_pattern)
- for iptables_variant in ['iptables', 'ip6tables']:
- # run iptables, save output and split it by lines
- iptables_command = f'{iptables_variant} -t {table} -S {chain}'
- tmp = cmd(iptables_command, message='Failed to get flows list')
+ # 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():
- current_rule_parsed = rule_re.search(current_rule)
- if current_rule_parsed:
- rules.append({ 'interface': current_rule_parsed.groupdict()["interface"], 'iptables_variant': iptables_variant, 'table': table, 'rule_definition': current_rule_parsed.groupdict()["rule_definition"] })
+ # 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
-# modify iptables rules
-def _iptables_config(configured_ifaces, direction):
- # define list of iptables commands to modify settings
- iptable_commands = []
- iptables_chain = iptables_nflog_chain
- iptables_table = iptables_nflog_table
+# modify nftables rules
+def _nftables_config(configured_ifaces, direction):
+ # define list of nftables commands to modify settings
+ nftable_commands = []
+ nftables_chain = nftables_nflog_chain
+ nftables_table = nftables_nflog_table
if direction == "egress":
- iptables_chain = egress_iptables_nflog_chain
- iptables_table = egress_iptables_nflog_table
+ 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, 'iptables_variant': 'iptables' })
- configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'ip6tables' })
+ configured_ifaces_extended.append({ 'iface': iface })
- # get currently configured interfaces with iptables rules
- active_nflog_rules = _iptables_get_nflog(iptables_chain, iptables_table)
+ # 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:
- iptables = rule['iptables_variant']
interface = rule['interface']
if interface not in configured_ifaces:
table = rule['table']
- rule = rule['rule_definition']
- iptable_commands.append(f'{iptables} -t {table} -D {rule}')
+ handle = rule['handle']
+ nftable_commands.append(f'nft delete rule {table} {nftables_chain} handle {handle}')
else:
active_nflog_ifaces.append({
'iface': interface,
- 'iptables_variant': iptables,
})
# do not create new rules for already configured interfaces
@@ -141,16 +140,12 @@ def _iptables_config(configured_ifaces, direction):
# create missed rules
for iface_extended in configured_ifaces_extended:
iface = iface_extended['iface']
- iptables = iface_extended['iptables_variant']
- iptables_op = "-i"
- if direction == "egress":
- iptables_op = "-o"
+ iface_prefix = "o" if direction == "egress" else "i"
+ rule_definition = f'{iface_prefix}ifname "{iface}" counter log group 2 snaplen {default_captured_packet_size} queue-threshold 100 comment "FLOW_ACCOUNTING_RULE"'
+ nftable_commands.append(f'nft insert rule {nftables_table} {nftables_chain} {rule_definition}')
- rule_definition = f'{iptables_chain} {iptables_op} {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100'
- iptable_commands.append(f'{iptables} -t {iptables_table} -I {rule_definition}')
-
- # change iptables
- for command in iptable_commands:
+ # change nftables
+ for command in nftable_commands:
cmd(command, raising=ConfigError)
@@ -367,18 +362,18 @@ def apply(config):
# run command to start or stop flow-accounting
cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting')
- # configure iptables rules for defined interfaces
+ # configure nftables rules for defined interfaces
if config['interfaces']:
- _iptables_config(config['interfaces'], 'ingress')
+ _nftables_config(config['interfaces'], 'ingress')
# configure egress the same way if configured otherwise remove it
if config['enable-egress']:
- _iptables_config(config['interfaces'], 'egress')
+ _nftables_config(config['interfaces'], 'egress')
else:
- _iptables_config([], 'egress')
+ _nftables_config([], 'egress')
else:
- _iptables_config([], 'ingress')
- _iptables_config([], 'egress')
+ _nftables_config([], 'ingress')
+ _nftables_config([], 'egress')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 59939d0fb..62fb9abad 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -93,7 +93,6 @@ def get_config(config=None):
nat[direction]['rule'][rule] = dict_merge(default_values,
nat[direction]['rule'][rule])
-
# read in current nftable (once) for further processing
tmp = cmd('nft -j list table raw')
nftable_json = json.loads(tmp)
@@ -106,9 +105,9 @@ def get_config(config=None):
nat['helper_functions'] = 'remove'
# Retrieve current table handler positions
- nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ 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', 'VYATTA_CT_HELPER')
+ 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
@@ -119,10 +118,10 @@ def get_config(config=None):
nat['helper_functions'] = 'add'
# Retrieve current table handler positions
- nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
- nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
- nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
- nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+ 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
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index fb376a434..d0b6d27ac 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -79,9 +79,9 @@ def get_config(config=None):
if not conf.exists(base):
nat['helper_functions'] = 'remove'
- nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ 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', 'VYATTA_CT_HELPER')
+ 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
@@ -92,10 +92,10 @@ def get_config(config=None):
nat['helper_functions'] = 'add'
# Retrieve current table handler positions
- nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
- nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
- nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
- nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+ 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'
diff --git a/src/conf_mode/protocols_nhrp.py b/src/conf_mode/protocols_nhrp.py
index 12dacdba0..7eeb5cd30 100755
--- a/src/conf_mode/protocols_nhrp.py
+++ b/src/conf_mode/protocols_nhrp.py
@@ -16,6 +16,8 @@
from vyos.config import Config
from vyos.configdict import node_changed
+from vyos.firewall import find_nftables_rule
+from vyos.firewall import remove_nftables_rule
from vyos.template import render
from vyos.util import process_named_running
from vyos.util import run
@@ -88,24 +90,19 @@ def generate(nhrp):
def apply(nhrp):
if 'tunnel' in nhrp:
for tunnel, tunnel_conf in nhrp['tunnel'].items():
- if 'source_address' in tunnel_conf:
- chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK'
- source_address = tunnel_conf['source_address']
+ if 'source_address' in nhrp['if_tunnel'][tunnel]:
+ comment = f'VYOS_NHRP_{tunnel}'
+ source_address = nhrp['if_tunnel'][tunnel]['source_address']
- chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0
- if not chain_exists:
- run(f'sudo iptables --new {chain}')
- run(f'sudo iptables --append {chain} -p gre -s {source_address} -d 224.0.0.0/4 -j DROP')
- run(f'sudo iptables --append {chain} -j RETURN')
- run(f'sudo iptables --insert OUTPUT 2 -j {chain}')
+ rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', ['ip protocol gre', f'ip saddr {source_address}', 'ip daddr 224.0.0.0/4'])
+ if not rule_handle:
+ run(f'sudo nft insert rule ip filter VYOS_FW_OUTPUT ip protocol gre ip saddr {source_address} ip daddr 224.0.0.0/4 counter drop comment "{comment}"')
for tunnel in nhrp['del_tunnels']:
- chain = f'VYOS_NHRP_{tunnel}_OUT_HOOK'
- chain_exists = run(f'sudo iptables --check {chain} -j RETURN') == 0
- if chain_exists:
- run(f'sudo iptables --delete OUTPUT -j {chain}')
- run(f'sudo iptables --flush {chain}')
- run(f'sudo iptables --delete-chain {chain}')
+ comment = f'VYOS_NHRP_{tunnel}'
+ rule_handle = find_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', [f'comment "{comment}"'])
+ if rule_handle:
+ remove_nftables_rule('ip filter', 'VYOS_FW_OUTPUT', rule_handle)
action = 'restart' if nhrp and 'tunnel' in nhrp else 'stop'
run(f'systemctl {action} opennhrp')
diff --git a/src/migration-scripts/firewall/6-to-7 b/src/migration-scripts/firewall/6-to-7
new file mode 100755
index 000000000..4a4097d56
--- /dev/null
+++ b/src/migration-scripts/firewall/6-to-7
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+# T2199: Remove unavailable nodes due to XML/Python implementation using nftables
+# monthdays: nftables does not have a monthdays equivalent
+# utc: nftables userspace uses localtime and calculates the UTC offset automatically
+
+from sys import argv
+from sys import exit
+
+from vyos.configtree import ConfigTree
+from vyos.ifconfig import Section
+
+if (len(argv) < 1):
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+base = ['firewall']
+config = ConfigTree(config_file)
+
+if not config.exists(base):
+ # Nothing to do
+ exit(0)
+
+if config.exists(base + ['name']):
+ for name in config.list_nodes(base + ['name']):
+ if config.exists(base + ['name', name, 'rule']):
+ for rule in config.list_nodes(base + ['name', name, 'rule']):
+ rule_time = base + ['name', name, 'rule', rule, 'time']
+
+ if config.exists(rule_time + ['monthdays']):
+ config.delete(rule_time + ['monthdays'])
+
+ if config.exists(rule_time + ['utc']):
+ config.delete(rule_time + ['utc'])
+
+if config.exists(base + ['ipv6-name']):
+ for name in config.list_nodes(base + ['ipv6-name']):
+ if config.exists(base + ['ipv6-name', name, 'rule']):
+ for rule in config.list_nodes(base + ['ipv6-name', name, 'rule']):
+ rule_time = base + ['ipv6-name', name, 'rule', rule, 'time']
+
+ if config.exists(rule_time + ['monthdays']):
+ config.delete(rule_time + ['monthdays'])
+
+ if config.exists(rule_time + ['utc']):
+ config.delete(rule_time + ['utc'])
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ exit(1)
diff --git a/src/validators/ip-protocol b/src/validators/ip-protocol
index 078f8e319..7898fa6d0 100755
--- a/src/validators/ip-protocol
+++ b/src/validators/ip-protocol
@@ -31,7 +31,7 @@ if __name__ == '__main__':
pattern = "!?\\b(all|ip|hopopt|icmp|igmp|ggp|ipencap|st|tcp|egp|igp|pup|udp|" \
"tcp_udp|hmp|xns-idp|rdp|iso-tp4|dccp|xtp|ddp|idpr-cmtp|ipv6|" \
- "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|" \
+ "ipv6-route|ipv6-frag|idrp|rsvp|gre|esp|ah|skip|ipv6-icmp|icmpv6|" \
"ipv6-nonxt|ipv6-opts|rspf|vmtp|eigrp|ospf|ax.25|ipip|etherip|" \
"encap|99|pim|ipcomp|vrrp|l2tp|isis|sctp|fc|mobility-header|" \
"udplite|mpls-in-ip|manet|hip|shim6|wesp|rohc)\\b"
--
cgit v1.2.3
From 85710cee8fe970cab1699f781df876761bd3cd3c Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Thu, 12 Aug 2021 00:56:41 +0200
Subject: firewall: T2199: Migrate firewall op-mode to XML/Python
---
op-mode-definitions/firewall.xml.in | 178 ++++++++++++++++++
src/op_mode/firewall.py | 355 ++++++++++++++++++++++++++++++++++++
2 files changed, 533 insertions(+)
create mode 100644 op-mode-definitions/firewall.xml.in
create mode 100755 src/op_mode/firewall.py
(limited to 'src')
diff --git a/op-mode-definitions/firewall.xml.in b/op-mode-definitions/firewall.xml.in
new file mode 100644
index 000000000..84df67b3d
--- /dev/null
+++ b/op-mode-definitions/firewall.xml.in
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+
+
+ Show firewall information
+
+
+
+
+ Show firewall group
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show_group --name $4
+
+
+
+ Show IPv6 firewall chains
+
+ firewall ipv6-name
+
+
+
+
+
+ Show summary of IPv6 firewall rules
+
+ firewall ipv6-name ${COMP_WORDS[6]} rule
+
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4 --rule $6 --ipv6
+
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4 --ipv6
+
+
+
+ Show IPv4 firewall chains
+
+ firewall name
+
+
+
+
+
+ Show summary of IPv4 firewall rules
+
+ firewall name ${COMP_WORDS[6]} rule
+
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4 --rule $6
+
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show --name $4
+
+
+
+ Show statistics of firewall application
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show_statistics
+
+
+
+ Show summary of firewall application
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show_summary
+
+
+ sudo ${vyos_op_scripts_dir}/firewall.py --action show_all
+
+
+
+
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
new file mode 100755
index 000000000..cf70890a6
--- /dev/null
+++ b/src/op_mode/firewall.py
@@ -0,0 +1,355 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import argparse
+import json
+import re
+import tabulate
+
+from vyos.config import Config
+from vyos.util import cmd
+from vyos.util import dict_search_args
+
+def get_firewall_interfaces(conf, firewall, name=None, ipv6=False):
+ interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ directions = ['in', 'out', 'local']
+
+ def parse_if(ifname, if_conf):
+ if 'firewall' in if_conf:
+ for direction in directions:
+ if direction in if_conf['firewall']:
+ fw_conf = if_conf['firewall'][direction]
+ name_str = f'({ifname},{direction})'
+
+ if 'name' in fw_conf:
+ fw_name = fw_conf['name']
+
+ if not name:
+ firewall['name'][fw_name]['interface'].append(name_str)
+ elif not ipv6 and name == fw_name:
+ firewall['interface'].append(name_str)
+
+ if 'ipv6_name' in fw_conf:
+ fw_name = fw_conf['ipv6_name']
+
+ if not name:
+ firewall['ipv6_name'][fw_name]['interface'].append(name_str)
+ elif ipv6 and name == fw_name:
+ firewall['interface'].append(name_str)
+
+ for iftype in ['vif', 'vif_s', 'vif_c']:
+ if iftype in if_conf:
+ for vifname, vif_conf in if_conf[iftype].items():
+ parse_if(f'{ifname}.{vifname}', vif_conf)
+
+ for iftype, iftype_conf in interfaces.items():
+ for ifname, if_conf in iftype_conf.items():
+ parse_if(ifname, if_conf)
+
+ return firewall
+
+def get_config_firewall(conf, name=None, ipv6=False, interfaces=True):
+ config_path = ['firewall']
+ if name:
+ config_path += ['ipv6-name' if ipv6 else 'name', name]
+
+ firewall = conf.get_config_dict(config_path, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ if firewall and interfaces:
+ if name:
+ firewall['interface'] = []
+ else:
+ if 'name' in firewall:
+ for fw_name, name_conf in firewall['name'].items():
+ name_conf['interface'] = []
+
+ if 'ipv6_name' in firewall:
+ for fw_name, name_conf in firewall['ipv6_name'].items():
+ name_conf['interface'] = []
+
+ get_firewall_interfaces(conf, firewall, name, ipv6)
+ return firewall
+
+def get_nftables_details(name, ipv6=False):
+ suffix = '6' if ipv6 else ''
+ command = f'sudo nft list chain ip{suffix} filter {name}'
+ try:
+ results = cmd(command)
+ except:
+ return {}
+
+ out = {}
+ for line in results.split('\n'):
+ comment_search = re.search(rf'{name}[\- ](\d+|default-action)', line)
+ if not comment_search:
+ continue
+
+ rule = {}
+ rule_id = comment_search[1]
+ counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line)
+ if counter_search:
+ rule['packets'] = counter_search[1]
+ rule['bytes'] = counter_search[2]
+
+ rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip()
+ out[rule_id] = rule
+ return out
+
+def output_firewall_name(name, name_conf, ipv6=False, single_rule_id=None):
+ ip_str = 'IPv6' if ipv6 else 'IPv4'
+ print(f'\n---------------------------------\n{ip_str} Firewall "{name}"\n')
+
+ if name_conf['interface']:
+ print('Active on: {0}\n'.format(" ".join(name_conf['interface'])))
+
+ details = get_nftables_details(name, ipv6)
+ rows = []
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ if single_rule_id and rule_id != single_rule_id:
+ continue
+
+ if 'disable' in rule_conf:
+ continue
+
+ row = [rule_id, rule_conf['action'], rule_conf['protocol'] if 'protocol' in rule_conf else 'all']
+ if rule_id in details:
+ rule_details = details[rule_id]
+ row.append(rule_details.get('packets', 0))
+ row.append(rule_details.get('bytes', 0))
+ row.append(rule_details['conditions'])
+ rows.append(row)
+
+ if 'default_action' in name_conf and not single_rule_id:
+ row = ['default', name_conf['default_action'], 'all']
+ if 'default-action' in details:
+ rule_details = details['default-action']
+ row.append(rule_details.get('packets', 0))
+ row.append(rule_details.get('bytes', 0))
+ rows.append(row)
+
+ if rows:
+ header = ['Rule', 'Action', 'Protocol', 'Packets', 'Bytes', 'Conditions']
+ print(tabulate.tabulate(rows, header) + '\n')
+
+def output_firewall_name_statistics(name, name_conf, ipv6=False, single_rule_id=None):
+ ip_str = 'IPv6' if ipv6 else 'IPv4'
+ print(f'\n---------------------------------\n{ip_str} Firewall "{name}"\n')
+
+ if name_conf['interface']:
+ print('Active on: {0}\n'.format(" ".join(name_conf['interface'])))
+
+ details = get_nftables_details(name, ipv6)
+ rows = []
+
+ if 'rule' in name_conf:
+ for rule_id, rule_conf in name_conf['rule'].items():
+ if single_rule_id and rule_id != single_rule_id:
+ continue
+
+ if 'disable' in rule_conf:
+ continue
+
+ source_addr = dict_search_args(rule_conf, 'source', 'address') or '0.0.0.0/0'
+ dest_addr = dict_search_args(rule_conf, 'destination', 'address') or '0.0.0.0/0'
+
+ row = [rule_id]
+ if rule_id in details:
+ rule_details = details[rule_id]
+ row.append(rule_details.get('packets', 0))
+ row.append(rule_details.get('bytes', 0))
+ else:
+ row.append('0')
+ row.append('0')
+ row.append(rule_conf['action'])
+ row.append(source_addr)
+ row.append(dest_addr)
+ rows.append(row)
+
+ if 'default_action' in name_conf and not single_rule_id:
+ row = ['default']
+ if 'default-action' in details:
+ rule_details = details['default-action']
+ row.append(rule_details.get('packets', 0))
+ row.append(rule_details.get('bytes', 0))
+ else:
+ row.append('0')
+ row.append('0')
+ row.append(name_conf['default_action'])
+ row.append('0.0.0.0/0') # Source
+ row.append('0.0.0.0/0') # Dest
+ rows.append(row)
+
+ if rows:
+ header = ['Rule', 'Packets', 'Bytes', 'Action', 'Source', 'Destination']
+ print(tabulate.tabulate(rows, header) + '\n')
+
+def show_firewall():
+ print('Rulesets Information')
+
+ conf = Config()
+ firewall = get_config_firewall(conf)
+
+ if not firewall:
+ return
+
+ if 'name' in firewall:
+ for name, name_conf in firewall['name'].items():
+ output_firewall_name(name, name_conf, ipv6=False)
+
+ if 'ipv6_name' in firewall:
+ for name, name_conf in firewall['ipv6_name'].items():
+ output_firewall_name(name, name_conf, ipv6=True)
+
+def show_firewall_name(name, ipv6=False):
+ print('Ruleset Information')
+
+ conf = Config()
+ firewall = get_config_firewall(conf, name, ipv6)
+ if firewall:
+ output_firewall_name(name, firewall, ipv6)
+
+def show_firewall_rule(name, rule_id, ipv6=False):
+ print('Rule Information')
+
+ conf = Config()
+ firewall = get_config_firewall(conf, name, ipv6)
+ if firewall:
+ output_firewall_name(name, firewall, ipv6, rule_id)
+
+def show_firewall_group(name=None):
+ conf = Config()
+ firewall = get_config_firewall(conf, interfaces=False)
+
+ if 'group' not in firewall:
+ return
+
+ def find_references(group_type, group_name):
+ out = []
+ for name_type in ['name', 'ipv6_name']:
+ if name_type not in firewall:
+ continue
+ for name, name_conf in firewall[name_type].items():
+ if 'rule' not in name_conf:
+ continue
+ for rule_id, rule_conf in name_conf['rule'].items():
+ source_group = dict_search_args(rule_conf, 'source', 'group', group_type)
+ dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type)
+ if source_group and group_name == source_group:
+ out.append(f'{name}-{rule_id}')
+ elif dest_group and group_name == dest_group:
+ out.append(f'{name}-{rule_id}')
+ return out
+
+ header = ['Name', 'Type', 'References', 'Members']
+ rows = []
+
+ for group_type, group_type_conf in firewall['group'].items():
+ for group_name, group_conf in group_type_conf.items():
+ if name and name != group_name:
+ continue
+
+ references = find_references(group_type, group_name)
+ row = [group_name, group_type, ', '.join(references)]
+ if 'address' in group_conf:
+ row.append(", ".join(group_conf['address']))
+ elif 'network' in group_conf:
+ row.append(", ".join(group_conf['network']))
+ elif 'port' in group_conf:
+ row.append(", ".join(group_conf['port']))
+ rows.append(row)
+
+ if rows:
+ print('Firewall Groups\n')
+ print(tabulate.tabulate(rows, header))
+
+def show_summary():
+ print('Ruleset Summary')
+
+ conf = Config()
+ firewall = get_config_firewall(conf)
+
+ if not firewall:
+ return
+
+ header = ['Ruleset Name', 'Description', 'References']
+ v4_out = []
+ v6_out = []
+
+ if 'name' in firewall:
+ for name, name_conf in firewall['name'].items():
+ description = name_conf.get('description', '')
+ interfaces = ", ".join(name_conf['interface'])
+ v4_out.append([name, description, interfaces])
+
+ if 'ipv6_name' in firewall:
+ for name, name_conf in firewall['ipv6_name'].items():
+ description = name_conf.get('description', '')
+ interfaces = ", ".join(name_conf['interface'])
+ v6_out.append([name, description, interfaces])
+
+ if v6_out:
+ print('\nIPv6 name:\n')
+ print(tabulate.tabulate(v6_out, header) + '\n')
+
+ if v4_out:
+ print('\nIPv4 name:\n')
+ print(tabulate.tabulate(v4_out, header) + '\n')
+
+ show_firewall_group()
+
+def show_statistics():
+ print('Rulesets Statistics')
+
+ conf = Config()
+ firewall = get_config_firewall(conf)
+
+ if not firewall:
+ return
+
+ if 'name' in firewall:
+ for name, name_conf in firewall['name'].items():
+ output_firewall_name_statistics(name, name_conf, ipv6=False)
+
+ if 'ipv6_name' in firewall:
+ for name, name_conf in firewall['ipv6_name'].items():
+ output_firewall_name_statistics(name, name_conf, ipv6=True)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Action', required=False)
+ parser.add_argument('--name', help='Firewall name', required=False, action='store', nargs='?', default='')
+ parser.add_argument('--rule', help='Firewall Rule ID', required=False)
+ parser.add_argument('--ipv6', help='IPv6 toggle', action='store_true')
+
+ args = parser.parse_args()
+
+ if args.action == 'show':
+ if not args.rule:
+ show_firewall_name(args.name, args.ipv6)
+ else:
+ show_firewall_rule(args.name, args.rule, args.ipv6)
+ elif args.action == 'show_all':
+ show_firewall()
+ elif args.action == 'show_group':
+ show_firewall_group(args.name)
+ elif args.action == 'show_statistics':
+ show_statistics()
+ elif args.action == 'show_summary':
+ show_summary()
--
cgit v1.2.3
From c7cf7b941445c9280ac1ed7bbbd68159654560a6 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sat, 31 Jul 2021 19:23:44 +0200
Subject: zone-policy: T2199: Migrate zone-policy to XML/Python
---
data/templates/zone_policy/nftables.tmpl | 97 ++++++++++++++++
interface-definitions/zone-policy.xml.in | 94 ++++++++++++++++
smoketest/scripts/cli/test_zone_policy.py | 63 +++++++++++
src/conf_mode/zone_policy.py | 176 ++++++++++++++++++++++++++++++
4 files changed, 430 insertions(+)
create mode 100644 data/templates/zone_policy/nftables.tmpl
create mode 100644 interface-definitions/zone-policy.xml.in
create mode 100755 smoketest/scripts/cli/test_zone_policy.py
create mode 100755 src/conf_mode/zone_policy.py
(limited to 'src')
diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl
new file mode 100644
index 000000000..4575a721c
--- /dev/null
+++ b/data/templates/zone_policy/nftables.tmpl
@@ -0,0 +1,97 @@
+#!/usr/sbin/nft -f
+
+{% if cleanup_commands is defined %}
+{% for command in cleanup_commands %}
+{{ command }}
+{% endfor %}
+{% endif %}
+
+{% if zone is defined %}
+table ip filter {
+{% for zone_name, zone_conf in zone.items() if zone_conf.ipv4 %}
+{% if zone_conf.local_zone is defined %}
+ chain VZONE_{{ zone_name }}_IN {
+ iifname lo counter return
+{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endfor %}
+ counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+ }
+ chain VZONE_{{ zone_name }}_OUT {
+ oifname lo counter return
+{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.name is defined %}
+ oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
+ oifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endfor %}
+ counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+ }
+{% else %}
+ chain VZONE_{{ zone_name }} {
+ iifname { {{ zone_conf.interface | join(",") }} } counter return
+{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
+{% if zone[from_zone].local_zone is not defined %}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endif %}
+{% endfor %}
+ counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+ }
+{% endif %}
+{% endfor %}
+}
+
+table ip6 filter {
+{% for zone_name, zone_conf in zone.items() if zone_conf.ipv6 %}
+{% if zone_conf.local_zone is defined %}
+ chain VZONE6_{{ zone_name }}_IN {
+ iifname lo counter return
+{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endfor %}
+ counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+ }
+ chain VZONE6_{{ zone_name }}_OUT {
+ oifname lo counter return
+{% for from_zone, from_conf in zone_conf.from_local.items() if from_conf.firewall.ipv6_name is defined %}
+ oifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
+ oifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endfor %}
+ counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+ }
+{% else %}
+ chain VZONE6_{{ zone_name }} {
+ iifname { {{ zone_conf.interface | join(",") }} } counter return
+{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
+{% if zone[from_zone].local_zone is not defined %}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
+ iifname { {{ zone[from_zone].interface | join(",") }} } counter return
+{% endif %}
+{% endfor %}
+ counter {{ zone_conf.default_action if zone_conf.default_action is defined else 'drop' }}
+ }
+{% endif %}
+{% endfor %}
+}
+
+{% for zone_name, zone_conf in zone.items() %}
+{% if zone_conf.ipv4 %}
+{% if 'local_zone' in zone_conf %}
+insert rule ip filter VYOS_FW_LOCAL counter jump VZONE_{{ zone_name }}_IN
+insert rule ip filter VYOS_FW_OUTPUT counter jump VZONE_{{ zone_name }}_OUT
+{% else %}
+insert rule ip filter VYOS_FW_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE_{{ zone_name }}
+{% endif %}
+{% endif %}
+{% if zone_conf.ipv6 %}
+{% if 'local_zone' in zone_conf %}
+insert rule ip6 filter VYOS_FW6_LOCAL counter jump VZONE6_{{ zone_name }}_IN
+insert rule ip6 filter VYOS_FW6_OUTPUT counter jump VZONE6_{{ zone_name }}_OUT
+{% else %}
+insert rule ip6 filter VYOS_FW6_OUT oifname { {{ zone_conf.interface | join(',') }} } counter jump VZONE6_{{ zone_name }}
+{% endif %}
+{% endif %}
+{% endfor %}
+
+{% endif %}
diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in
new file mode 100644
index 000000000..52fd73f15
--- /dev/null
+++ b/interface-definitions/zone-policy.xml.in
@@ -0,0 +1,94 @@
+
+
+
+
+ Configure zone-policy
+ 250
+
+
+
+
+ Zone name
+
+ txt
+ Zone name
+
+
+
+ #include
+
+
+ Default-action for traffic coming into this zone
+
+ drop reject
+
+
+ drop
+ Drop silently (default)
+
+
+ reject
+ Drop and notify source
+
+
+ ^(drop|reject)$
+
+
+
+
+
+ Zone from which to filter traffic
+
+ zone-policy zone
+
+
+
+
+
+ Firewall options
+
+
+
+
+ IPv6 firewall ruleset
+
+ firewall ipv6-name
+
+
+
+
+
+ IPv4 firewall ruleset
+
+ firewall name
+
+
+
+
+
+
+
+
+
+ Interface associated with zone
+
+ txt
+ Interface associated with zone
+
+
+
+
+
+
+
+
+
+ Zone to be local-zone
+
+
+
+
+
+
+
+
diff --git a/smoketest/scripts/cli/test_zone_policy.py b/smoketest/scripts/cli/test_zone_policy.py
new file mode 100755
index 000000000..c0af6164b
--- /dev/null
+++ b/smoketest/scripts/cli/test_zone_policy.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.util import cmd
+
+class TestZonePolicy(VyOSUnitTestSHIM.TestCase):
+ def setUp(self):
+ self.cli_set(['firewall', 'name', 'smoketest', 'default-action', 'drop'])
+
+ def tearDown(self):
+ self.cli_delete(['zone-policy'])
+ self.cli_delete(['firewall'])
+ self.cli_commit()
+
+ def test_basic_zone(self):
+ self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'interface', 'eth0'])
+ self.cli_set(['zone-policy', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest'])
+ self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'local-zone'])
+ self.cli_set(['zone-policy', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['chain VZONE_smoketest-eth0'],
+ ['chain VZONE_smoketest-local_IN'],
+ ['chain VZONE_smoketest-local_OUT'],
+ ['oifname { "eth0" }', 'jump VZONE_smoketest-eth0'],
+ ['jump VZONE_smoketest-local_IN'],
+ ['jump VZONE_smoketest-local_OUT'],
+ ['iifname { "eth0" }', 'jump smoketest'],
+ ['oifname { "eth0" }', 'jump smoketest']
+ ]
+
+ nftables_output = cmd('sudo nft list table ip filter')
+
+ 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(matched)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
new file mode 100755
index 000000000..92f5624c2
--- /dev/null
+++ b/src/conf_mode/zone_policy.py
@@ -0,0 +1,176 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+
+from json import loads
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import cmd
+from vyos.util import dict_search_args
+from vyos.util import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+nftables_conf = '/run/nftables_zone.conf'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['zone-policy']
+ zone_policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ if zone_policy:
+ zone_policy['firewall'] = conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return zone_policy
+
+def verify(zone_policy):
+ # bail out early - looks like removal from running config
+ if not zone_policy:
+ return None
+
+ local_zone = False
+ interfaces = []
+
+ if 'zone' in zone_policy:
+ for zone, zone_conf in zone_policy['zone'].items():
+ if 'local_zone' not in zone_conf and 'interface' not in zone_conf:
+ raise ConfigError(f'Zone "{zone}" has no interfaces and is not the local zone')
+
+ if 'local_zone' in zone_conf:
+ if local_zone:
+ raise ConfigError('There cannot be multiple local zones')
+ if 'interface' in zone_conf:
+ raise ConfigError('Local zone cannot have interfaces assigned')
+ local_zone = True
+
+ if 'interface' in zone_conf:
+ found_duplicates = [intf for intf in zone_conf['interface'] if intf in interfaces]
+
+ if found_duplicates:
+ raise ConfigError(f'Interfaces cannot be assigned to multiple zones')
+
+ interfaces += zone_conf['interface']
+
+ if 'from' in zone_conf:
+ for from_zone, from_conf in zone_conf['from'].items():
+ v4_name = dict_search_args(from_conf, 'firewall', 'name')
+ if v4_name:
+ if 'name' not in zone_policy['firewall']:
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ if not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ v6_name = dict_search_args(from_conf, 'firewall', 'v6_name')
+ if v6_name:
+ if 'ipv6_name' not in zone_policy['firewall']:
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
+ if not dict_search_args(zone_policy, 'firewall', 'ipv6_name', v6_name):
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
+ return None
+
+def has_ipv4_fw(zone_conf):
+ if 'from' not in zone_conf:
+ return False
+ zone_from = zone_conf['from']
+ return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'name')])
+
+def has_ipv6_fw(zone_conf):
+ if 'from' not in zone_conf:
+ return False
+ zone_from = zone_conf['from']
+ return any([True for fz in zone_from if dict_search_args(zone_from, fz, 'firewall', 'ipv6_name')])
+
+def get_local_from(zone_policy, local_zone_name):
+ # Get all zone firewall names from the local zone
+ out = {}
+ for zone, zone_conf in zone_policy['zone'].items():
+ if zone == local_zone_name:
+ continue
+ if 'from' not in zone_conf:
+ continue
+ if local_zone_name in zone_conf['from']:
+ out[zone] = zone_conf['from'][local_zone_name]
+ return out
+
+def cleanup_commands():
+ commands = []
+ for table in ['ip filter', 'ip6 filter']:
+ json_str = cmd(f'nft -j list table {table}')
+ obj = loads(json_str)
+ if 'nftables' not in obj:
+ continue
+ for item in obj['nftables']:
+ if 'rule' in item:
+ chain = item['rule']['chain']
+ handle = item['rule']['handle']
+ if 'expr' not in item['rule']:
+ continue
+ for expr in item['rule']['expr']:
+ target = dict_search_args(expr, 'jump', 'target')
+ if target and target.startswith("VZONE"):
+ commands.append(f'delete rule {table} {chain} handle {handle}')
+ for item in obj['nftables']:
+ if 'chain' in item:
+ if item['chain']['name'].startswith("VZONE"):
+ chain = item['chain']['name']
+ commands.append(f'delete chain {table} {chain}')
+ return commands
+
+def generate(zone_policy):
+ data = zone_policy or {}
+
+ if os.path.exists(nftables_conf): # Check to see if we've run before
+ data['cleanup_commands'] = cleanup_commands()
+
+ if 'zone' in data:
+ for zone, zone_conf in data['zone'].items():
+ zone_conf['ipv4'] = has_ipv4_fw(zone_conf)
+ zone_conf['ipv6'] = has_ipv6_fw(zone_conf)
+
+ if 'local_zone' in zone_conf:
+ zone_conf['from_local'] = get_local_from(data, zone)
+
+ render(nftables_conf, 'zone_policy/nftables.tmpl', data)
+ return None
+
+def apply(zone_policy):
+ install_result = run(f'nft -f {nftables_conf}')
+ if install_result == 1:
+ raise ConfigError('Failed to apply zone-policy')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
--
cgit v1.2.3
From 3ebb08893b4bbd016bd0eb04374be5f691ad4abb Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Thu, 12 Aug 2021 01:00:59 +0200
Subject: zone-policy: T2199: Migrate zone-policy op-mode to XML/Python
---
op-mode-definitions/zone-policy.xml.in | 24 ++++++++++
src/op_mode/zone_policy.py | 81 ++++++++++++++++++++++++++++++++++
2 files changed, 105 insertions(+)
create mode 100644 op-mode-definitions/zone-policy.xml.in
create mode 100755 src/op_mode/zone_policy.py
(limited to 'src')
diff --git a/op-mode-definitions/zone-policy.xml.in b/op-mode-definitions/zone-policy.xml.in
new file mode 100644
index 000000000..c4b02bcee
--- /dev/null
+++ b/op-mode-definitions/zone-policy.xml.in
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+ Show zone policy information
+
+
+
+
+ Show summary of zone policy for a specific zone
+
+ zone-policy zone
+
+
+ sudo ${vyos_op_scripts_dir}/zone_policy.py --action show --name $4
+
+
+ sudo ${vyos_op_scripts_dir}/zone_policy.py --action show
+
+
+
+
diff --git a/src/op_mode/zone_policy.py b/src/op_mode/zone_policy.py
new file mode 100755
index 000000000..7b43018c2
--- /dev/null
+++ b/src/op_mode/zone_policy.py
@@ -0,0 +1,81 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import argparse
+import tabulate
+
+from vyos.config import Config
+from vyos.util import dict_search_args
+
+def get_config_zone(conf, name=None):
+ config_path = ['zone-policy']
+ if name:
+ config_path += ['zone', name]
+
+ zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ return zone_policy
+
+def output_zone_name(zone, zone_conf):
+ print(f'\n---------------------------------\nZone: "{zone}"\n')
+
+ interfaces = ', '.join(zone_conf['interface']) if 'interface' in zone_conf else ''
+ if 'local_zone' in zone_conf:
+ interfaces = 'LOCAL'
+
+ print(f'Interfaces: {interfaces}\n')
+
+ header = ['From Zone', 'Firewall']
+ rows = []
+
+ if 'from' in zone_conf:
+ for from_name, from_conf in zone_conf['from'].items():
+ row = [from_name]
+ v4_name = dict_search_args(from_conf, 'firewall', 'name')
+ v6_name = dict_search_args(from_conf, 'firewall', 'ipv6_name')
+
+ if v4_name:
+ rows.append(row + [v4_name])
+
+ if v6_name:
+ rows.append(row + [f'{v6_name} [IPv6]'])
+
+ if rows:
+ print('From Zones:\n')
+ print(tabulate.tabulate(rows, header))
+
+def show_zone_policy(zone):
+ conf = Config()
+ zone_policy = get_config_zone(conf, zone)
+
+ if not zone_policy:
+ return
+
+ if 'zone' in zone_policy:
+ for zone, zone_conf in zone_policy['zone'].items():
+ output_zone_name(zone, zone_conf)
+ elif zone:
+ output_zone_name(zone, zone_policy)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Action', required=False)
+ parser.add_argument('--name', help='Zone name', required=False, action='store', nargs='?', default='')
+
+ args = parser.parse_args()
+
+ if args.action == 'show':
+ show_zone_policy(args.name)
--
cgit v1.2.3
From f86041de88c3b0e0ce9ecc6d2cbc309bc8cb28e2 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 1 Aug 2021 00:13:47 +0200
Subject: policy: T2199: Migrate policy route to XML/Python
---
data/templates/firewall/nftables-policy.tmpl | 53 ++
.../include/interface/interface-policy-vif-c.xml.i | 26 +
.../include/interface/interface-policy-vif.xml.i | 26 +
.../include/interface/interface-policy.xml.i | 26 +
.../include/interface/vif-s.xml.i | 2 +
interface-definitions/include/interface/vif.xml.i | 1 +
.../include/policy/route-common-rule-ipv6.xml.i | 569 +++++++++++++++++++++
.../include/policy/route-common-rule.xml.i | 418 +++++++++++++++
.../include/policy/route-rule-action.xml.i | 17 +
interface-definitions/interfaces-bonding.xml.in | 1 +
interface-definitions/interfaces-bridge.xml.in | 1 +
interface-definitions/interfaces-dummy.xml.in | 1 +
interface-definitions/interfaces-ethernet.xml.in | 1 +
interface-definitions/interfaces-geneve.xml.in | 1 +
interface-definitions/interfaces-l2tpv3.xml.in | 1 +
interface-definitions/interfaces-macsec.xml.in | 1 +
interface-definitions/interfaces-openvpn.xml.in | 1 +
interface-definitions/interfaces-pppoe.xml.in | 1 +
.../interfaces-pseudo-ethernet.xml.in | 1 +
interface-definitions/interfaces-tunnel.xml.in | 1 +
interface-definitions/interfaces-vti.xml.in | 1 +
interface-definitions/interfaces-vxlan.xml.in | 1 +
interface-definitions/interfaces-wireguard.xml.in | 1 +
interface-definitions/interfaces-wireless.xml.in | 1 +
interface-definitions/interfaces-wwan.xml.in | 1 +
interface-definitions/policy-route.xml.in | 83 +++
python/vyos/firewall.py | 23 +
smoketest/scripts/cli/test_policy_route.py | 106 ++++
src/conf_mode/policy-route-interface.py | 120 +++++
src/conf_mode/policy-route.py | 154 ++++++
30 files changed, 1640 insertions(+)
create mode 100644 data/templates/firewall/nftables-policy.tmpl
create mode 100644 interface-definitions/include/interface/interface-policy-vif-c.xml.i
create mode 100644 interface-definitions/include/interface/interface-policy-vif.xml.i
create mode 100644 interface-definitions/include/interface/interface-policy.xml.i
create mode 100644 interface-definitions/include/policy/route-common-rule-ipv6.xml.i
create mode 100644 interface-definitions/include/policy/route-common-rule.xml.i
create mode 100644 interface-definitions/include/policy/route-rule-action.xml.i
create mode 100644 interface-definitions/policy-route.xml.in
create mode 100755 smoketest/scripts/cli/test_policy_route.py
create mode 100755 src/conf_mode/policy-route-interface.py
create mode 100755 src/conf_mode/policy-route.py
(limited to 'src')
diff --git a/data/templates/firewall/nftables-policy.tmpl b/data/templates/firewall/nftables-policy.tmpl
new file mode 100644
index 000000000..aa6bb6fc1
--- /dev/null
+++ b/data/templates/firewall/nftables-policy.tmpl
@@ -0,0 +1,53 @@
+#!/usr/sbin/nft -f
+
+table ip mangle {
+{% if first_install is defined %}
+ chain VYOS_PBR_PREROUTING {
+ type filter hook prerouting priority -150; policy accept;
+ }
+ chain VYOS_PBR_POSTROUTING {
+ type filter hook postrouting priority -150; policy accept;
+ }
+{% endif %}
+{% if route is defined -%}
+{% for route_text, conf in route.items() %}
+ chain VYOS_PBR_{{ route_text }} {
+{% if conf.rule is defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
+ {{ rule_conf | nft_rule(route_text, rule_id, 'ip') }}
+{% endfor %}
+{% endif %}
+{% if conf.default_action is defined %}
+ counter {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}"
+{% else %}
+ counter return
+{% endif %}
+ }
+{% endfor %}
+{%- endif %}
+}
+
+table ip6 mangle {
+{% if first_install is defined %}
+ chain VYOS_PBR6_PREROUTING {
+ type filter hook prerouting priority -150; policy accept;
+ }
+ chain VYOS_PBR6_POSTROUTING {
+ type filter hook postrouting priority -150; policy accept;
+ }
+{% endif %}
+{% if ipv6_route is defined %}
+{% for route_text, conf in ipv6_route.items() %}
+ chain VYOS_PBR6_{{ route_text }} {
+{% if conf.rule is defined %}
+{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not defined %}
+ {{ rule_conf | nft_rule(route_text, rule_id, 'ip6') }}
+{% endfor %}
+{% endif %}
+{% if conf.default_action is defined %}
+ counter {{ conf.default_action | nft_action }} comment "{{ name_text }} default-action {{ conf.default_action }}"
+{% endif %}
+ }
+{% endfor %}
+{% endif %}
+}
diff --git a/interface-definitions/include/interface/interface-policy-vif-c.xml.i b/interface-definitions/include/interface/interface-policy-vif-c.xml.i
new file mode 100644
index 000000000..5dad6422b
--- /dev/null
+++ b/interface-definitions/include/interface/interface-policy-vif-c.xml.i
@@ -0,0 +1,26 @@
+
+
+
+ 620
+ Policy route options
+
+
+
+
+ IPv4 policy route ruleset for interface
+
+ policy route
+
+
+
+
+
+ IPv6 policy route ruleset for interface
+
+ policy ipv6-route
+
+
+
+
+
+
diff --git a/interface-definitions/include/interface/interface-policy-vif.xml.i b/interface-definitions/include/interface/interface-policy-vif.xml.i
new file mode 100644
index 000000000..5ee80ae13
--- /dev/null
+++ b/interface-definitions/include/interface/interface-policy-vif.xml.i
@@ -0,0 +1,26 @@
+
+
+
+ 620
+ Policy route options
+
+
+
+
+ IPv4 policy route ruleset for interface
+
+ policy route
+
+
+
+
+
+ IPv6 policy route ruleset for interface
+
+ policy ipv6-route
+
+
+
+
+
+
diff --git a/interface-definitions/include/interface/interface-policy.xml.i b/interface-definitions/include/interface/interface-policy.xml.i
new file mode 100644
index 000000000..06f025af1
--- /dev/null
+++ b/interface-definitions/include/interface/interface-policy.xml.i
@@ -0,0 +1,26 @@
+
+
+
+ 620
+ Policy route options
+
+
+
+
+ IPv4 policy route ruleset for interface
+
+ policy route
+
+
+
+
+
+ IPv6 policy route ruleset for interface
+
+ policy ipv6-route
+
+
+
+
+
+
diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i
index caa5248ab..f1a61ff64 100644
--- a/interface-definitions/include/interface/vif-s.xml.i
+++ b/interface-definitions/include/interface/vif-s.xml.i
@@ -19,6 +19,7 @@
#include
#include
#include
+ #include
Protocol used for service VLAN (default: 802.1ad)
@@ -65,6 +66,7 @@
#include
#include
#include
+ #include
#include
diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i
index a2382cc1b..11ba7e2f8 100644
--- a/interface-definitions/include/interface/vif.xml.i
+++ b/interface-definitions/include/interface/vif.xml.i
@@ -20,6 +20,7 @@
#include
#include
#include
+ #include
VLAN egress QoS
diff --git a/interface-definitions/include/policy/route-common-rule-ipv6.xml.i b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i
new file mode 100644
index 000000000..2d6adcd1d
--- /dev/null
+++ b/interface-definitions/include/policy/route-common-rule-ipv6.xml.i
@@ -0,0 +1,569 @@
+
+#include
+#include
+
+
+ Option to disable firewall rule
+
+
+
+
+
+ IP fragment match
+
+
+
+
+ Second and further fragments of fragmented packets
+
+
+
+
+
+ Head fragments or unfragmented packets
+
+
+
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+
+ Inbound non-IPsec packets
+
+
+
+
+
+
+
+ Rate limit using a token bucket filter
+
+
+
+
+ Maximum number of packets to allow in excess of rate
+
+ u32:0-4294967295
+ Maximum number of packets to allow in excess of rate
+
+
+
+
+
+
+
+
+ Maximum average matching rate
+
+ u32:0-4294967295
+ Maximum average matching rate
+
+
+
+
+
+
+
+
+
+
+ Option to log packets matching rule
+
+ enable disable
+
+
+ enable
+ Enable log
+
+
+ disable
+ Disable log
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Protocol to match (protocol name, number, or "all")
+
+
+
+
+ all
+ All IP protocols
+
+
+ tcp_udp
+ Both TCP and UDP
+
+
+ 0-255
+ IP protocol number
+
+
+ !<protocol>
+ IP protocol number
+
+
+
+
+
+ all
+
+
+
+ Parameters for matching recently seen sources
+
+
+
+
+ Source addresses seen more than N times
+
+ u32:1-255
+ Source addresses seen more than N times
+
+
+
+
+
+
+
+
+ Source addresses seen in the last N seconds
+
+ u32:0-4294967295
+ Source addresses seen in the last N seconds
+
+
+
+
+
+
+
+
+
+
+ Packet modifications
+
+
+
+
+ Packet Differentiated Services Codepoint (DSCP)
+
+ u32:0-63
+ DSCP number
+
+
+
+
+
+
+
+
+ Packet marking
+
+ u32:1-2147483647
+ Packet marking
+
+
+
+
+
+
+
+
+ Routing table to forward packet with
+
+ u32:1-200
+ Table number
+
+
+ main
+ Main table
+
+
+
+ ^(main)$
+
+
+
+
+
+ TCP Maximum Segment Size
+
+ u32:500-1460
+ Explicitly set TCP MSS value
+
+
+
+
+
+
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+
+
+ Source MAC address
+
+ <MAC address>
+ MAC address to match
+
+
+ !<MAC address>
+ Match everything except the specified MAC address
+
+
+
+ #include
+
+
+
+
+ Session state
+
+
+
+
+ Established state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Invalid state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ New state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Related state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+
+
+ TCP flags to match
+
+
+
+
+ TCP flags to match
+
+ txt
+ TCP flags to match
+
+
+
+ \n\n Allowed values for TCP flags : SYN ACK FIN RST URG PSH ALL\n When specifying more than one flag, flags should be comma-separated.\n For example : value of 'SYN,!ACK,!FIN,!RST' will only match packets with\n the SYN flag set, and the ACK, FIN and RST flags unset
+
+
+
+
+
+
+
+ Time to match rule
+
+
+
+
+ Monthdays to match rule on
+
+
+
+
+ Date to start matching rule
+
+
+
+
+ Time of day to start matching rule
+
+
+
+
+ Date to stop matching rule
+
+
+
+
+ Time of day to stop matching rule
+
+
+
+
+ Interpret times for startdate, stopdate, starttime and stoptime to be UTC
+
+
+
+
+
+ Weekdays to match rule on
+
+
+
+
+
+
+ ICMPv6 type and code information
+
+
+
+
+ ICMP type-name
+
+ any echo-reply pong destination-unreachable network-unreachable host-unreachable protocol-unreachable port-unreachable fragmentation-needed source-route-failed network-unknown host-unknown network-prohibited host-prohibited TOS-network-unreachable TOS-host-unreachable communication-prohibited host-precedence-violation precedence-cutoff source-quench redirect network-redirect host-redirect TOS-network-redirect TOS host-redirect echo-request ping router-advertisement router-solicitation time-exceeded ttl-exceeded ttl-zero-during-transit ttl-zero-during-reassembly parameter-problem ip-header-bad required-option-missing timestamp-request timestamp-reply address-mask-request address-mask-reply packet-too-big
+
+
+ any
+ Any ICMP type/code
+
+
+ echo-reply
+ ICMP type/code name
+
+
+ pong
+ ICMP type/code name
+
+
+ destination-unreachable
+ ICMP type/code name
+
+
+ network-unreachable
+ ICMP type/code name
+
+
+ host-unreachable
+ ICMP type/code name
+
+
+ protocol-unreachable
+ ICMP type/code name
+
+
+ port-unreachable
+ ICMP type/code name
+
+
+ fragmentation-needed
+ ICMP type/code name
+
+
+ source-route-failed
+ ICMP type/code name
+
+
+ network-unknown
+ ICMP type/code name
+
+
+ host-unknown
+ ICMP type/code name
+
+
+ network-prohibited
+ ICMP type/code name
+
+
+ host-prohibited
+ ICMP type/code name
+
+
+ TOS-network-unreachable
+ ICMP type/code name
+
+
+ TOS-host-unreachable
+ ICMP type/code name
+
+
+ communication-prohibited
+ ICMP type/code name
+
+
+ host-precedence-violation
+ ICMP type/code name
+
+
+ precedence-cutoff
+ ICMP type/code name
+
+
+ source-quench
+ ICMP type/code name
+
+
+ redirect
+ ICMP type/code name
+
+
+ network-redirect
+ ICMP type/code name
+
+
+ host-redirect
+ ICMP type/code name
+
+
+ TOS-network-redirect
+ ICMP type/code name
+
+
+ TOS host-redirect
+ ICMP type/code name
+
+
+ echo-request
+ ICMP type/code name
+
+
+ ping
+ ICMP type/code name
+
+
+ router-advertisement
+ ICMP type/code name
+
+
+ router-solicitation
+ ICMP type/code name
+
+
+ time-exceeded
+ ICMP type/code name
+
+
+ ttl-exceeded
+ ICMP type/code name
+
+
+ ttl-zero-during-transit
+ ICMP type/code name
+
+
+ ttl-zero-during-reassembly
+ ICMP type/code name
+
+
+ parameter-problem
+ ICMP type/code name
+
+
+ ip-header-bad
+ ICMP type/code name
+
+
+ required-option-missing
+ ICMP type/code name
+
+
+ timestamp-request
+ ICMP type/code name
+
+
+ timestamp-reply
+ ICMP type/code name
+
+
+ address-mask-request
+ ICMP type/code name
+
+
+ address-mask-reply
+ ICMP type/code name
+
+
+ packet-too-big
+ ICMP type/code name
+
+
+ ^(any|echo-reply|pong|destination-unreachable|network-unreachable|host-unreachable|protocol-unreachable|port-unreachable|fragmentation-needed|source-route-failed|network-unknown|host-unknown|network-prohibited|host-prohibited|TOS-network-unreachable|TOS-host-unreachable|communication-prohibited|host-precedence-violation|precedence-cutoff|source-quench|redirect|network-redirect|host-redirect|TOS-network-redirect|TOS host-redirect|echo-request|ping|router-advertisement|router-solicitation|time-exceeded|ttl-exceeded|ttl-zero-during-transit|ttl-zero-during-reassembly|parameter-problem|ip-header-bad|required-option-missing|timestamp-request|timestamp-reply|address-mask-request|address-mask-reply|packet-too-big)$
+
+
+
+
+
+
+
diff --git a/interface-definitions/include/policy/route-common-rule.xml.i b/interface-definitions/include/policy/route-common-rule.xml.i
new file mode 100644
index 000000000..c4deefd2a
--- /dev/null
+++ b/interface-definitions/include/policy/route-common-rule.xml.i
@@ -0,0 +1,418 @@
+
+#include
+#include
+
+
+ Option to disable firewall rule
+
+
+
+
+
+ IP fragment match
+
+
+
+
+ Second and further fragments of fragmented packets
+
+
+
+
+
+ Head fragments or unfragmented packets
+
+
+
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+ Inbound IPsec packets
+
+
+
+
+
+ Inbound non-IPsec packets
+
+
+
+
+
+
+
+ Rate limit using a token bucket filter
+
+
+
+
+ Maximum number of packets to allow in excess of rate
+
+ u32:0-4294967295
+ Maximum number of packets to allow in excess of rate
+
+
+
+
+
+
+
+
+ Maximum average matching rate
+
+ u32:0-4294967295
+ Maximum average matching rate
+
+
+
+
+
+
+
+
+
+
+ Option to log packets matching rule
+
+ enable disable
+
+
+ enable
+ Enable log
+
+
+ disable
+ Disable log
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Protocol to match (protocol name, number, or "all")
+
+
+
+
+ all
+ All IP protocols
+
+
+ tcp_udp
+ Both TCP and UDP
+
+
+ 0-255
+ IP protocol number
+
+
+ !<protocol>
+ IP protocol number
+
+
+
+
+
+ all
+
+
+
+ Parameters for matching recently seen sources
+
+
+
+
+ Source addresses seen more than N times
+
+ u32:1-255
+ Source addresses seen more than N times
+
+
+
+
+
+
+
+
+ Source addresses seen in the last N seconds
+
+ u32:0-4294967295
+ Source addresses seen in the last N seconds
+
+
+
+
+
+
+
+
+
+
+ Packet modifications
+
+
+
+
+ Packet Differentiated Services Codepoint (DSCP)
+
+ u32:0-63
+ DSCP number
+
+
+
+
+
+
+
+
+ Packet marking
+
+ u32:1-2147483647
+ Packet marking
+
+
+
+
+
+
+
+
+ Routing table to forward packet with
+
+ u32:1-200
+ Table number
+
+
+ main
+ Main table
+
+
+
+ ^(main)$
+
+
+
+
+
+ TCP Maximum Segment Size
+
+ u32:500-1460
+ Explicitly set TCP MSS value
+
+
+
+
+
+
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+
+
+ Source MAC address
+
+ <MAC address>
+ MAC address to match
+
+
+ !<MAC address>
+ Match everything except the specified MAC address
+
+
+
+ #include
+
+
+
+
+ Session state
+
+
+
+
+ Established state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Invalid state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ New state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+ Related state
+
+ enable disable
+
+
+ enable
+ Enable
+
+
+ disable
+ Disable
+
+
+ ^(enable|disable)$
+
+
+
+
+
+
+
+ TCP flags to match
+
+
+
+
+ TCP flags to match
+
+ txt
+ TCP flags to match
+
+
+
+ \n\n Allowed values for TCP flags : SYN ACK FIN RST URG PSH ALL\n When specifying more than one flag, flags should be comma-separated.\n For example : value of 'SYN,!ACK,!FIN,!RST' will only match packets with\n the SYN flag set, and the ACK, FIN and RST flags unset
+
+
+
+
+
+
+
+ Time to match rule
+
+
+
+
+ Monthdays to match rule on
+
+
+
+
+ Date to start matching rule
+
+
+
+
+ Time of day to start matching rule
+
+
+
+
+ Date to stop matching rule
+
+
+
+
+ Time of day to stop matching rule
+
+
+
+
+ Interpret times for startdate, stopdate, starttime and stoptime to be UTC
+
+
+
+
+
+ Weekdays to match rule on
+
+
+
+
+
+
+ ICMP type and code information
+
+
+
+
+ ICMP code (0-255)
+
+ u32:0-255
+ ICMP code (0-255)
+
+
+
+
+
+
+
+
+ ICMP type (0-255)
+
+ u32:0-255
+ ICMP type (0-255)
+
+
+
+
+
+
+ #include
+
+
+
diff --git a/interface-definitions/include/policy/route-rule-action.xml.i b/interface-definitions/include/policy/route-rule-action.xml.i
new file mode 100644
index 000000000..9c880579d
--- /dev/null
+++ b/interface-definitions/include/policy/route-rule-action.xml.i
@@ -0,0 +1,17 @@
+
+
+
+ Rule action [REQUIRED]
+
+ drop
+
+
+ drop
+ Drop matching entries
+
+
+ ^(drop)$
+
+
+
+
diff --git a/interface-definitions/interfaces-bonding.xml.in b/interface-definitions/interfaces-bonding.xml.in
index 03cbb523d..723041ca5 100644
--- a/interface-definitions/interfaces-bonding.xml.in
+++ b/interface-definitions/interfaces-bonding.xml.in
@@ -57,6 +57,7 @@
#include
#include
#include
+ #include
Bonding transmit hash policy
diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in
index ebf6c3631..0856615be 100644
--- a/interface-definitions/interfaces-bridge.xml.in
+++ b/interface-definitions/interfaces-bridge.xml.in
@@ -42,6 +42,7 @@
#include
#include
#include
+ #include
Forwarding delay
diff --git a/interface-definitions/interfaces-dummy.xml.in b/interface-definitions/interfaces-dummy.xml.in
index c6061b8bb..1231b1492 100644
--- a/interface-definitions/interfaces-dummy.xml.in
+++ b/interface-definitions/interfaces-dummy.xml.in
@@ -20,6 +20,7 @@
#include
#include
#include
+ #include
IPv4 routing parameters
diff --git a/interface-definitions/interfaces-ethernet.xml.in b/interface-definitions/interfaces-ethernet.xml.in
index 3868ebbbc..9e113cb71 100644
--- a/interface-definitions/interfaces-ethernet.xml.in
+++ b/interface-definitions/interfaces-ethernet.xml.in
@@ -32,6 +32,7 @@
#include
#include
#include
+ #include
Duplex mode
diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in
index 06ad7c82b..dd4d324d4 100644
--- a/interface-definitions/interfaces-geneve.xml.in
+++ b/interface-definitions/interfaces-geneve.xml.in
@@ -24,6 +24,7 @@
#include
#include
#include
+ #include
GENEVE tunnel parameters
diff --git a/interface-definitions/interfaces-l2tpv3.xml.in b/interface-definitions/interfaces-l2tpv3.xml.in
index c5bca4408..85d4ab992 100644
--- a/interface-definitions/interfaces-l2tpv3.xml.in
+++ b/interface-definitions/interfaces-l2tpv3.xml.in
@@ -33,6 +33,7 @@
#include
#include
+ #include
Encapsulation type (default: UDP)
diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in
index 5713d985b..d69a093af 100644
--- a/interface-definitions/interfaces-macsec.xml.in
+++ b/interface-definitions/interfaces-macsec.xml.in
@@ -20,6 +20,7 @@
#include
#include
#include
+ #include
Security/Encryption Settings
diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in
index 1fe8e63f8..16d91145f 100644
--- a/interface-definitions/interfaces-openvpn.xml.in
+++ b/interface-definitions/interfaces-openvpn.xml.in
@@ -35,6 +35,7 @@
#include
#include
+ #include
OpenVPN interface device-type (default: tun)
diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in
index d9c30031e..80a890940 100644
--- a/interface-definitions/interfaces-pppoe.xml.in
+++ b/interface-definitions/interfaces-pppoe.xml.in
@@ -20,6 +20,7 @@
#include
#include
#include
+ #include
Default route insertion behaviour (default: auto)
diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in
index 974ba1a50..bf7055f8d 100644
--- a/interface-definitions/interfaces-pseudo-ethernet.xml.in
+++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in
@@ -28,6 +28,7 @@
#include
#include
#include
+ #include
Receive mode (default: private)
diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in
index b95f07a4b..e08b2fdab 100644
--- a/interface-definitions/interfaces-tunnel.xml.in
+++ b/interface-definitions/interfaces-tunnel.xml.in
@@ -31,6 +31,7 @@
#include
#include
#include
+ #include
6rd network prefix
diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in
index a8a330f32..f03c7476d 100644
--- a/interface-definitions/interfaces-vti.xml.in
+++ b/interface-definitions/interfaces-vti.xml.in
@@ -36,6 +36,7 @@
#include
#include
#include
+ #include
diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in
index c5bb0c8f2..0c13dd4d3 100644
--- a/interface-definitions/interfaces-vxlan.xml.in
+++ b/interface-definitions/interfaces-vxlan.xml.in
@@ -42,6 +42,7 @@
#include
#include
#include
+ #include
1450
diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in
index c96b3d46b..7a7c9c1d9 100644
--- a/interface-definitions/interfaces-wireguard.xml.in
+++ b/interface-definitions/interfaces-wireguard.xml.in
@@ -23,6 +23,7 @@
#include
#include
#include
+ #include
1420
diff --git a/interface-definitions/interfaces-wireless.xml.in b/interface-definitions/interfaces-wireless.xml.in
index da739840b..a2d1439a3 100644
--- a/interface-definitions/interfaces-wireless.xml.in
+++ b/interface-definitions/interfaces-wireless.xml.in
@@ -18,6 +18,7 @@
#include
#include
+ #include
HT and VHT capabilities for your card
diff --git a/interface-definitions/interfaces-wwan.xml.in b/interface-definitions/interfaces-wwan.xml.in
index 926c48194..03554feed 100644
--- a/interface-definitions/interfaces-wwan.xml.in
+++ b/interface-definitions/interfaces-wwan.xml.in
@@ -40,6 +40,7 @@
#include
#include
#include
+ #include
diff --git a/interface-definitions/policy-route.xml.in b/interface-definitions/policy-route.xml.in
new file mode 100644
index 000000000..ed726d1e4
--- /dev/null
+++ b/interface-definitions/policy-route.xml.in
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+ IPv6 policy route rule set name
+ 201
+
+
+ #include
+ #include
+
+
+ Rule number (1-9999)
+
+
+
+
+ Destination parameters
+
+
+ #include
+ #include
+ #include
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+ #include
+
+
+ #include
+
+
+
+
+
+
+ Policy route rule set name
+ 201
+
+
+ #include
+ #include
+
+
+ Rule number (1-9999)
+
+
+
+
+ Destination parameters
+
+
+ #include
+ #include
+ #include
+
+
+
+
+ Source parameters
+
+
+ #include
+ #include
+ #include
+
+
+ #include
+
+
+
+
+
+
+
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 9b8af7852..8b7402b7e 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -150,8 +150,12 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name):
if tcp_flags:
output.append(parse_tcp_flags(tcp_flags))
+
output.append('counter')
+ if 'set' in rule_conf:
+ output.append(parse_policy_set(rule_conf['set'], def_suffix))
+
if 'action' in rule_conf:
output.append(nft_action(rule_conf['action']))
else:
@@ -192,3 +196,22 @@ def parse_time(time):
out_days = [f'"{day}"' for day in days if day[0] != '!']
out.append(f'day {{{",".join(out_days)}}}')
return " ".join(out)
+
+def parse_policy_set(set_conf, def_suffix):
+ out = []
+ if 'dscp' in set_conf:
+ dscp = set_conf['dscp']
+ out.append(f'ip{def_suffix} dscp set {dscp}')
+ if 'mark' in set_conf:
+ mark = set_conf['mark']
+ out.append(f'meta mark set {mark}')
+ if 'table' in set_conf:
+ table = set_conf['table']
+ if table == 'main':
+ table = '254'
+ mark = 0x7FFFFFFF - int(set_conf['table'])
+ out.append(f'meta mark set {mark}')
+ if 'tcp_mss' in set_conf:
+ mss = set_conf['tcp_mss']
+ out.append(f'tcp option maxseg size set {mss}')
+ return " ".join(out)
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
new file mode 100755
index 000000000..70a234187
--- /dev/null
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import unittest
+
+from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.util import cmd
+
+mark = '100'
+table_mark_offset = 0x7fffffff
+table_id = '101'
+
+class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
+ def setUp(self):
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '172.16.10.1/24'])
+ self.cli_set(['protocols', 'static', 'table', '101', 'route', '0.0.0.0/0', 'interface', 'eth0'])
+
+ def tearDown(self):
+ self.cli_delete(['interfaces', 'ethernet', 'eth0'])
+ self.cli_delete(['policy', 'route'])
+ self.cli_delete(['policy', 'ipv6-route'])
+ self.cli_commit()
+
+ def test_pbr_mark(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'address', '172.16.20.10'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'address', '172.16.10.10'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'mark', mark])
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest'])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(int(mark))
+
+ nftables_search = [
+ ['iifname "eth0"', 'jump VYOS_PBR_smoketest'],
+ ['ip daddr 172.16.10.10', 'ip saddr 172.16.20.10', 'meta mark set ' + mark_hex],
+ ]
+
+ nftables_output = cmd('sudo nft list table ip mangle')
+
+ 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(matched)
+
+ def test_pbr_table(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'protocol', 'tcp_udp'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'destination', 'port', '8888'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'set', 'table', table_id])
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'policy', 'route', 'smoketest'])
+
+ self.cli_commit()
+
+ mark_hex = "{0:#010x}".format(table_mark_offset - int(table_id))
+
+ nftables_search = [
+ ['iifname "eth0"', 'jump VYOS_PBR_smoketest'],
+ ['meta l4proto { tcp, udp }', 'th dport { 8888 }', 'meta mark set ' + mark_hex]
+ ]
+
+ nftables_output = cmd('sudo nft list table ip mangle')
+
+ 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(matched)
+
+ ip_rule_search = [
+ ['fwmark ' + hex(table_mark_offset - int(table_id)), 'lookup ' + table_id]
+ ]
+
+ ip_rule_output = cmd('ip rule show')
+
+ for search in ip_rule_search:
+ matched = False
+ for line in ip_rule_output.split("\n"):
+ if all(item in line for item in search):
+ matched = True
+ break
+ self.assertTrue(matched)
+
+
+if __name__ == '__main__':
+ unittest.main(verbosity=2)
diff --git a/src/conf_mode/policy-route-interface.py b/src/conf_mode/policy-route-interface.py
new file mode 100755
index 000000000..e81135a74
--- /dev/null
+++ b/src/conf_mode/policy-route-interface.py
@@ -0,0 +1,120 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+import re
+
+from sys import argv
+from sys import exit
+
+from vyos.config import Config
+from vyos.ifconfig import Section
+from vyos.template import render
+from vyos.util import cmd
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ ifname = argv[1]
+ ifpath = Section.get_config_path(ifname)
+ if_policy_path = f'interfaces {ifpath} policy'
+
+ if_policy = conf.get_config_dict(if_policy_path, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ if_policy['ifname'] = ifname
+ if_policy['policy'] = conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return if_policy
+
+def verify(if_policy):
+ # bail out early - looks like removal from running config
+ if not if_policy:
+ return None
+
+ for route in ['route', 'ipv6_route']:
+ if route in if_policy:
+ if route not in if_policy['policy']:
+ raise ConfigError('Policy route not configured')
+
+ route_name = if_policy[route]
+
+ if route_name not in if_policy['policy'][route]:
+ raise ConfigError(f'Invalid policy route name "{name}"')
+
+ return None
+
+def generate(if_policy):
+ return None
+
+def cleanup_rule(table, chain, ifname, new_name=None):
+ results = cmd(f'nft -a list chain {table} {chain}').split("\n")
+ retval = None
+ for line in results:
+ if f'oifname "{ifname}"' in line:
+ if new_name and f'jump {new_name}' in line:
+ # new_name is used to clear rules for any previously referenced chains
+ # returns true when rule exists and doesn't need to be created
+ retval = True
+ continue
+
+ handle_search = re.search('handle (\d+)', line)
+ if handle_search:
+ cmd(f'nft delete rule {table} {chain} handle {handle_search[1]}')
+ return retval
+
+def apply(if_policy):
+ ifname = if_policy['ifname']
+
+ route_chain = 'VYOS_PBR_PREROUTING'
+ ipv6_route_chain = 'VYOS_PBR6_PREROUTING'
+
+ if 'route' in if_policy:
+ name = 'VYOS_PBR_' + if_policy['route']
+ rule_exists = cleanup_rule('ip mangle', route_chain, ifname, name)
+
+ if not rule_exists:
+ cmd(f'nft insert rule ip mangle {route_chain} iifname {ifname} counter jump {name}')
+ else:
+ cleanup_rule('ip mangle', route_chain, ifname)
+
+ if 'ipv6_route' in if_policy:
+ name = 'VYOS_PBR6_' + if_policy['ipv6_route']
+ rule_exists = cleanup_rule('ip6 mangle', ipv6_route_chain, ifname, name)
+
+ if not rule_exists:
+ cmd(f'nft insert rule ip6 mangle {ipv6_route_chain} iifname {ifname} counter jump {name}')
+ else:
+ cleanup_rule('ip6 mangle', ipv6_route_chain, ifname)
+
+ 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/policy-route.py b/src/conf_mode/policy-route.py
new file mode 100755
index 000000000..d098be68d
--- /dev/null
+++ b/src/conf_mode/policy-route.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import os
+
+from json import loads
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import cmd
+from vyos.util import dict_search_args
+from vyos.util import run
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+mark_offset = 0x7FFFFFFF
+nftables_conf = '/run/nftables_policy.conf'
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+ base = ['policy']
+
+ if not conf.exists(base + ['route']) and not conf.exists(base + ['ipv6-route']):
+ return None
+
+ policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True)
+
+ return policy
+
+def verify(policy):
+ # bail out early - looks like removal from running config
+ if not policy:
+ return None
+
+ for route in ['route', 'ipv6_route']:
+ if route in policy:
+ for name, pol_conf in policy[route].items():
+ if 'rule' in pol_conf:
+ for rule_id, rule_conf in pol_conf.items():
+ icmp = 'icmp' if route == 'route' else 'icmpv6'
+ if icmp in rule_conf:
+ icmp_defined = False
+ if 'type_name' in rule_conf[icmp]:
+ icmp_defined = True
+ if 'code' in rule_conf[icmp] or 'type' in rule_conf[icmp]:
+ raise ConfigError(f'{name} rule {rule_id}: Cannot use ICMP type/code with ICMP type-name')
+ if 'code' in rule_conf[icmp]:
+ icmp_defined = True
+ if 'type' not in rule_conf[icmp]:
+ raise ConfigError(f'{name} rule {rule_id}: ICMP code can only be defined if ICMP type is defined')
+ if 'type' in rule_conf[icmp]:
+ icmp_defined = True
+
+ if icmp_defined and 'protocol' not in rule_conf or rule_conf['protocol'] != icmp:
+ raise ConfigError(f'{name} rule {rule_id}: ICMP type/code or type-name can only be defined if protocol is ICMP')
+ if 'set' in rule_conf:
+ if 'tcp_mss' in rule_conf['set']:
+ tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
+ if not tcp_flags or 'SYN' not in tcp_flags.split(","):
+ raise ConfigError(f'{name} rule {rule_id}: TCP SYN flag must be set to modify TCP-MSS')
+ if 'tcp' in rule_conf:
+ if 'flags' in rule_conf['tcp']:
+ if 'protocol' not in rule_conf or rule_conf['protocol'] != 'tcp':
+ raise ConfigError(f'{name} rule {rule_id}: TCP flags can only be set if protocol is set to TCP')
+
+
+ return None
+
+def generate(policy):
+ if not policy:
+ if os.path.exists(nftables_conf):
+ os.unlink(nftables_conf)
+ return None
+
+ if not os.path.exists(nftables_conf):
+ policy['first_install'] = True
+
+ render(nftables_conf, 'firewall/nftables-policy.tmpl', policy)
+ return None
+
+def apply_table_marks(policy):
+ for route in ['route', 'ipv6_route']:
+ if route in policy:
+ for name, pol_conf in policy[route].items():
+ if 'rule' in pol_conf:
+ for rule_id, rule_conf in pol_conf['rule'].items():
+ set_table = dict_search_args(rule_conf, 'set', 'table')
+ if set_table:
+ if set_table == 'main':
+ set_table = '254'
+ table_mark = mark_offset - int(set_table)
+ cmd(f'ip rule add fwmark {table_mark} table {set_table}')
+
+def cleanup_table_marks():
+ json_rules = cmd('ip -j -N rule list')
+ rules = loads(json_rules)
+ for rule in rules:
+ if 'fwmark' not in rule or 'table' not in rule:
+ continue
+ fwmark = rule['fwmark']
+ table = int(rule['table'])
+ if fwmark[:2] == '0x':
+ fwmark = int(fwmark, 16)
+ if (int(fwmark) == (mark_offset - table)):
+ cmd(f'ip rule del fwmark {fwmark} table {table}')
+
+def apply(policy):
+ if not policy or 'first_install' not in policy:
+ run(f'nft flush table ip mangle')
+ run(f'nft flush table ip6 mangle')
+
+ if not policy:
+ cleanup_table_marks()
+ return None
+
+ install_result = run(f'nft -f {nftables_conf}')
+ if install_result == 1:
+ raise ConfigError('Failed to apply policy based routing')
+
+ if 'first_install' not in policy:
+ cleanup_table_marks()
+
+ apply_table_marks(policy)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
--
cgit v1.2.3
From dcd202aeeb890edf289c57305cb0bf07c87df341 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Thu, 12 Aug 2021 00:57:00 +0200
Subject: policy: T2199: Migrate policy route op-mode to XML/Python
---
op-mode-definitions/policy-route.xml.in | 143 ++++++++++++++++++++++++
src/op_mode/policy_route.py | 189 ++++++++++++++++++++++++++++++++
2 files changed, 332 insertions(+)
create mode 100644 op-mode-definitions/policy-route.xml.in
create mode 100755 src/op_mode/policy_route.py
(limited to 'src')
diff --git a/op-mode-definitions/policy-route.xml.in b/op-mode-definitions/policy-route.xml.in
new file mode 100644
index 000000000..c998e5487
--- /dev/null
+++ b/op-mode-definitions/policy-route.xml.in
@@ -0,0 +1,143 @@
+
+
+
+
+
+
+
+ Show policy information
+
+
+
+
+ Show IPv6 policy chain
+
+ sudo ${vyos_op_scripts_dir}/policy_route.py --action show_all --ipv6
+
+
+
+ Show IPv6 policy chains
+
+ policy ipv6-route
+
+
+
+
+
+ Show summary of IPv6 policy rules
+
+ policy ipv6-route ${COMP_WORDS[4]} rule
+
+
+ sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4 --rule $6 --ipv6
+
+
+ sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4 --ipv6
+
+
+
+ Show IPv4 policy chain
+
+ sudo ${vyos_op_scripts_dir}/policy_route.py --action show_all
+
+
+
+ Show IPv4 policy chains
+
+ policy route
+
+
+
+
+
+ Show summary of IPv4 policy rules
+
+ policy route ${COMP_WORDS[4]} rule
+
+
+ sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4 --rule $6
+
+
+ sudo ${vyos_op_scripts_dir}/policy_route.py --action show --name $4
+
+
+
+
+
+
diff --git a/src/op_mode/policy_route.py b/src/op_mode/policy_route.py
new file mode 100755
index 000000000..e0b4ac514
--- /dev/null
+++ b/src/op_mode/policy_route.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2021 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import argparse
+import re
+import tabulate
+
+from vyos.config import Config
+from vyos.util import cmd
+from vyos.util import dict_search_args
+
+def get_policy_interfaces(conf, policy, name=None, ipv6=False):
+ interfaces = conf.get_config_dict(['interfaces'], key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+
+ routes = ['route', 'ipv6_route']
+
+ def parse_if(ifname, if_conf):
+ if 'policy' in if_conf:
+ for route in routes:
+ if route in if_conf['policy']:
+ route_name = if_conf['policy'][route]
+ name_str = f'({ifname},{route})'
+
+ if not name:
+ policy[route][route_name]['interface'].append(name_str)
+ elif not ipv6 and name == route_name:
+ policy['interface'].append(name_str)
+
+ for iftype in ['vif', 'vif_s', 'vif_c']:
+ if iftype in if_conf:
+ for vifname, vif_conf in if_conf[iftype].items():
+ parse_if(f'{ifname}.{vifname}', vif_conf)
+
+ for iftype, iftype_conf in interfaces.items():
+ for ifname, if_conf in iftype_conf.items():
+ parse_if(ifname, if_conf)
+
+def get_config_policy(conf, name=None, ipv6=False, interfaces=True):
+ config_path = ['policy']
+ if name:
+ config_path += ['ipv6-route' if ipv6 else 'route', name]
+
+ policy = conf.get_config_dict(config_path, key_mangling=('-', '_'),
+ get_first_key=True, no_tag_node_value_mangle=True)
+ if policy and interfaces:
+ if name:
+ policy['interface'] = []
+ else:
+ if 'route' in policy:
+ for route_name, route_conf in policy['route'].items():
+ route_conf['interface'] = []
+
+ if 'ipv6_route' in policy:
+ for route_name, route_conf in policy['ipv6_route'].items():
+ route_conf['interface'] = []
+
+ get_policy_interfaces(conf, policy, name, ipv6)
+
+ return policy
+
+def get_nftables_details(name, ipv6=False):
+ suffix = '6' if ipv6 else ''
+ command = f'sudo nft list chain ip{suffix} mangle VYOS_PBR{suffix}_{name}'
+ try:
+ results = cmd(command)
+ except:
+ return {}
+
+ out = {}
+ for line in results.split('\n'):
+ comment_search = re.search(rf'{name}[\- ](\d+|default-action)', line)
+ if not comment_search:
+ continue
+
+ rule = {}
+ rule_id = comment_search[1]
+ counter_search = re.search(r'counter packets (\d+) bytes (\d+)', line)
+ if counter_search:
+ rule['packets'] = counter_search[1]
+ rule['bytes'] = counter_search[2]
+
+ rule['conditions'] = re.sub(r'(\b(counter packets \d+ bytes \d+|drop|reject|return|log)\b|comment "[\w\-]+")', '', line).strip()
+ out[rule_id] = rule
+ return out
+
+def output_policy_route(name, route_conf, ipv6=False, single_rule_id=None):
+ ip_str = 'IPv6' if ipv6 else 'IPv4'
+ print(f'\n---------------------------------\n{ip_str} Policy Route "{name}"\n')
+
+ if route_conf['interface']:
+ print('Active on: {0}\n'.format(" ".join(route_conf['interface'])))
+
+ details = get_nftables_details(name, ipv6)
+ rows = []
+
+ if 'rule' in route_conf:
+ for rule_id, rule_conf in route_conf['rule'].items():
+ if single_rule_id and rule_id != single_rule_id:
+ continue
+
+ if 'disable' in rule_conf:
+ continue
+
+ action = rule_conf['action'] if 'action' in rule_conf else 'set'
+ protocol = rule_conf['protocol'] if 'protocol' in rule_conf else 'all'
+
+ row = [rule_id, action, protocol]
+ if rule_id in details:
+ rule_details = details[rule_id]
+ row.append(rule_details.get('packets', 0))
+ row.append(rule_details.get('bytes', 0))
+ row.append(rule_details['conditions'])
+ rows.append(row)
+
+ if 'default_action' in route_conf and not single_rule_id:
+ row = ['default', route_conf['default_action'], 'all']
+ if 'default-action' in details:
+ rule_details = details['default-action']
+ row.append(rule_details.get('packets', 0))
+ row.append(rule_details.get('bytes', 0))
+ rows.append(row)
+
+ if rows:
+ header = ['Rule', 'Action', 'Protocol', 'Packets', 'Bytes', 'Conditions']
+ print(tabulate.tabulate(rows, header) + '\n')
+
+def show_policy(ipv6=False):
+ print('Ruleset Information')
+
+ conf = Config()
+ policy = get_config_policy(conf)
+
+ if not policy:
+ return
+
+ if not ipv6 and 'route' in policy:
+ for route, route_conf in policy['route'].items():
+ output_policy_route(route, route_conf, ipv6=False)
+
+ if ipv6 and 'ipv6_route' in policy:
+ for route, route_conf in policy['ipv6_route'].items():
+ output_policy_route(route, route_conf, ipv6=True)
+
+def show_policy_name(name, ipv6=False):
+ print('Ruleset Information')
+
+ conf = Config()
+ policy = get_config_policy(conf, name, ipv6)
+ if policy:
+ output_policy_route(name, policy, ipv6)
+
+def show_policy_rule(name, rule_id, ipv6=False):
+ print('Rule Information')
+
+ conf = Config()
+ policy = get_config_policy(conf, name, ipv6)
+ if policy:
+ output_policy_route(name, policy, ipv6, rule_id)
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--action', help='Action', required=False)
+ parser.add_argument('--name', help='Policy name', required=False, action='store', nargs='?', default='')
+ parser.add_argument('--rule', help='Policy Rule ID', required=False)
+ parser.add_argument('--ipv6', help='IPv6 toggle', action='store_true')
+
+ args = parser.parse_args()
+
+ if args.action == 'show':
+ if not args.rule:
+ show_policy_name(args.name, args.ipv6)
+ else:
+ show_policy_rule(args.name, args.rule, args.ipv6)
+ elif args.action == 'show_all':
+ show_policy(args.ipv6)
--
cgit v1.2.3
From 28b285b4791aece18fe1bbd76f3d555370545006 Mon Sep 17 00:00:00 2001
From: sarthurdev <965089+sarthurdev@users.noreply.github.com>
Date: Sun, 31 Oct 2021 21:24:40 +0100
Subject: zone_policy: T3873: Implement intra-zone-filtering
---
data/templates/zone_policy/nftables.tmpl | 4 +--
interface-definitions/zone-policy.xml.in | 49 ++++++++++++++++++++++++++++++++
python/vyos/template.py | 15 ++++++++++
src/conf_mode/zone_policy.py | 20 +++++++++++++
4 files changed, 86 insertions(+), 2 deletions(-)
(limited to 'src')
diff --git a/data/templates/zone_policy/nftables.tmpl b/data/templates/zone_policy/nftables.tmpl
index 4575a721c..21230c688 100644
--- a/data/templates/zone_policy/nftables.tmpl
+++ b/data/templates/zone_policy/nftables.tmpl
@@ -28,7 +28,7 @@ table ip filter {
}
{% else %}
chain VZONE_{{ zone_name }} {
- iifname { {{ zone_conf.interface | join(",") }} } counter return
+ iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=False) }}
{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.name is defined %}
{% if zone[from_zone].local_zone is not defined %}
iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.name }}
@@ -62,7 +62,7 @@ table ip6 filter {
}
{% else %}
chain VZONE6_{{ zone_name }} {
- iifname { {{ zone_conf.interface | join(",") }} } counter return
+ iifname { {{ zone_conf.interface | join(",") }} } counter {{ zone_conf | nft_intra_zone_action(ipv6=True) }}
{% for from_zone, from_conf in zone_conf.from.items() if from_conf.firewall.ipv6_name is defined %}
{% if zone[from_zone].local_zone is not defined %}
iifname { {{ zone[from_zone].interface | join(",") }} } counter jump {{ from_conf.firewall.ipv6_name }}
diff --git a/interface-definitions/zone-policy.xml.in b/interface-definitions/zone-policy.xml.in
index 52fd73f15..dd64c7c16 100644
--- a/interface-definitions/zone-policy.xml.in
+++ b/interface-definitions/zone-policy.xml.in
@@ -81,6 +81,55 @@
+
+
+ Intra-zone filtering
+
+
+
+
+ Action for intra-zone traffic
+
+ accept drop
+
+
+ accept
+ Accept traffic (default)
+
+
+ drop
+ Drop silently
+
+
+ ^(accept|drop)$
+
+
+
+
+
+ Use the specified firewall chain
+
+
+
+
+ IPv6 firewall ruleset
+
+ firewall ipv6-name
+
+
+
+
+
+ IPv4 firewall ruleset
+
+ firewall name
+
+
+
+
+
+
+
Zone to be local-zone
diff --git a/python/vyos/template.py b/python/vyos/template.py
index 55bd04136..e20890e25 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -505,3 +505,18 @@ def nft_state_policy(conf, state):
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'
+
+ 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}'
+ return 'return'
diff --git a/src/conf_mode/zone_policy.py b/src/conf_mode/zone_policy.py
index 92f5624c2..2535ea33b 100755
--- a/src/conf_mode/zone_policy.py
+++ b/src/conf_mode/zone_policy.py
@@ -63,6 +63,8 @@ def verify(zone_policy):
raise ConfigError('There cannot be multiple local zones')
if 'interface' in zone_conf:
raise ConfigError('Local zone cannot have interfaces assigned')
+ if 'intra_zone_filtering' in zone_conf:
+ raise ConfigError('Local zone cannot use intra-zone-filtering')
local_zone = True
if 'interface' in zone_conf:
@@ -73,6 +75,24 @@ def verify(zone_policy):
interfaces += zone_conf['interface']
+ if 'intra_zone_filtering' in zone_conf:
+ intra_zone = zone_conf['intra_zone_filtering']
+
+ if len(intra_zone) > 1:
+ raise ConfigError('Only one intra-zone-filtering action must be specified')
+
+ if 'firewall' in intra_zone:
+ v4_name = dict_search_args(intra_zone, 'firewall', 'name')
+ if v4_name and not dict_search_args(zone_policy, 'firewall', 'name', v4_name):
+ raise ConfigError(f'Firewall name "{v4_name}" does not exist')
+
+ v6_name = dict_search_args(intra_zone, 'firewall', 'ipv6-name')
+ if v6_name and not dict_search_args(zone_policy, 'firewall', 'ipv6-name', v6_name):
+ raise ConfigError(f'Firewall ipv6-name "{v6_name}" does not exist')
+
+ if not v4_name and not v6_name:
+ raise ConfigError('No firewall names specified for intra-zone-filtering')
+
if 'from' in zone_conf:
for from_zone, from_conf in zone_conf['from'].items():
v4_name = dict_search_args(from_conf, 'firewall', 'name')
--
cgit v1.2.3