summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/config_mgmt.py32
-rw-r--r--python/vyos/configdep.py61
-rw-r--r--python/vyos/firewall.py23
-rw-r--r--python/vyos/ifconfig/control.py14
-rw-r--r--python/vyos/ifconfig/interface.py167
-rw-r--r--python/vyos/ifconfig/vxlan.py66
-rw-r--r--python/vyos/template.py78
-rw-r--r--python/vyos/utils/network.py173
-rw-r--r--python/vyos/utils/process.py4
9 files changed, 459 insertions, 159 deletions
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index 0fc72e660..dbf17ade4 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -25,12 +25,14 @@ from datetime import datetime
from textwrap import dedent
from pathlib import Path
from tabulate import tabulate
+from shutil import copy
from vyos.config import Config
from vyos.configtree import ConfigTree, ConfigTreeError, show_diff
from vyos.defaults import directories
from vyos.version import get_full_version_data
from vyos.utils.io import ask_yes_no
+from vyos.utils.boot import boot_configuration_complete
from vyos.utils.process import is_systemd_service_active
from vyos.utils.process import rc_cmd
@@ -200,9 +202,9 @@ Proceed ?'''
raise ConfigMgmtError(out)
entry = self._read_tmp_log_entry()
- self._add_log_entry(**entry)
if self._archive_active_config():
+ self._add_log_entry(**entry)
self._update_archive()
msg = 'Reboot timer stopped'
@@ -223,8 +225,6 @@ Proceed ?'''
def rollback(self, rev: int, no_prompt: bool=False) -> Tuple[str,int]:
"""Reboot to config revision 'rev'.
"""
- from shutil import copy
-
msg = ''
if not self._check_revision_number(rev):
@@ -334,10 +334,10 @@ Proceed ?'''
user = self._get_user()
via = 'init'
comment = ''
- self._add_log_entry(user, via, comment)
# add empty init config before boot-config load for revision
# and diff consistency
if self._archive_active_config():
+ self._add_log_entry(user, via, comment)
self._update_archive()
os.umask(mask)
@@ -352,9 +352,8 @@ Proceed ?'''
self._new_log_entry(tmp_file=tmp_log_entry)
return
- self._add_log_entry()
-
if self._archive_active_config():
+ self._add_log_entry()
self._update_archive()
def commit_archive(self):
@@ -475,22 +474,26 @@ Proceed ?'''
conf_file.chmod(0o644)
def _archive_active_config(self) -> bool:
+ save_to_tmp = (boot_configuration_complete() or not
+ os.path.isfile(archive_config_file))
mask = os.umask(0o113)
ext = os.getpid()
- tmp_save = f'/tmp/config.boot.{ext}'
- save_config(tmp_save)
+ cmp_saved = f'/tmp/config.boot.{ext}'
+ if save_to_tmp:
+ save_config(cmp_saved)
+ else:
+ copy(config_file, cmp_saved)
try:
- if cmp(tmp_save, archive_config_file, shallow=False):
- # this will be the case on boot, as well as certain
- # re-initialiation instances after delete/set
- os.unlink(tmp_save)
+ if cmp(cmp_saved, archive_config_file, shallow=False):
+ os.unlink(cmp_saved)
+ os.umask(mask)
return False
except FileNotFoundError:
pass
- rc, out = rc_cmd(f'sudo mv {tmp_save} {archive_config_file}')
+ rc, out = rc_cmd(f'sudo mv {cmp_saved} {archive_config_file}')
os.umask(mask)
if rc != 0:
@@ -522,9 +525,8 @@ Proceed ?'''
return len(l)
def _check_revision_number(self, rev: int) -> bool:
- # exclude init revision:
maxrev = self._get_number_of_revisions()
- if not 0 <= rev < maxrev - 1:
+ if not 0 <= rev < maxrev:
return False
return True
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
index 7a8559839..05d9a3fa3 100644
--- a/python/vyos/configdep.py
+++ b/python/vyos/configdep.py
@@ -1,4 +1,4 @@
-# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -17,8 +17,10 @@ import os
import json
import typing
from inspect import stack
+from graphlib import TopologicalSorter, CycleError
from vyos.utils.system import load_as_module
+from vyos.configdict import dict_merge
from vyos.defaults import directories
from vyos.configsource import VyOSError
from vyos import ConfigError
@@ -28,6 +30,9 @@ from vyos import ConfigError
if typing.TYPE_CHECKING:
from vyos.config import Config
+dependency_dir = os.path.join(directories['data'],
+ 'config-mode-dependencies')
+
dependent_func: dict[str, list[typing.Callable]] = {}
def canon_name(name: str) -> str:
@@ -40,12 +45,20 @@ def canon_name_of_path(path: str) -> str:
def caller_name() -> str:
return stack()[-1].filename
-def read_dependency_dict() -> dict:
- path = os.path.join(directories['data'],
- 'config-mode-dependencies.json')
- with open(path) as f:
- d = json.load(f)
- return d
+def read_dependency_dict(dependency_dir: str = dependency_dir) -> dict:
+ res = {}
+ for dep_file in os.listdir(dependency_dir):
+ if not dep_file.endswith('.json'):
+ continue
+ path = os.path.join(dependency_dir, dep_file)
+ with open(path) as f:
+ d = json.load(f)
+ if dep_file == 'vyos-1x.json':
+ res = dict_merge(res, d)
+ else:
+ res = dict_merge(d, res)
+
+ return res
def get_dependency_dict(config: 'Config') -> dict:
if hasattr(config, 'cached_dependency_dict'):
@@ -93,3 +106,37 @@ def call_dependents():
while l:
f = l.pop(0)
f()
+
+def graph_from_dependency_dict(d: dict) -> dict:
+ g = {}
+ for k in list(d):
+ g[k] = set()
+ # add the dependencies for every sub-case; should there be cases
+ # that are mutally exclusive in the future, the graphs will be
+ # distinguished
+ for el in list(d[k]):
+ g[k] |= set(d[k][el])
+
+ return g
+
+def is_acyclic(d: dict) -> bool:
+ g = graph_from_dependency_dict(d)
+ ts = TopologicalSorter(g)
+ try:
+ # get node iterator
+ order = ts.static_order()
+ # try iteration
+ _ = [*order]
+ except CycleError:
+ return False
+
+ return True
+
+def check_dependency_graph(dependency_dir: str = dependency_dir,
+ supplement: str = None) -> bool:
+ d = read_dependency_dict(dependency_dir=dependency_dir)
+ if supplement is not None:
+ with open(supplement) as f:
+ d = dict_merge(json.load(f), d)
+
+ return is_acyclic(d)
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 53ff8259e..3305eb269 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -87,7 +87,14 @@ def nft_action(vyos_action):
def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output = []
- def_suffix = '6' if ip_name == 'ip6' else ''
+ #def_suffix = '6' if ip_name == 'ip6' else ''
+
+ if ip_name == 'ip6':
+ def_suffix = '6'
+ family = 'ipv6'
+ else:
+ def_suffix = ''
+ family = 'bri' if ip_name == 'bri' else 'ipv4'
if 'state' in rule_conf and rule_conf['state']:
states = ",".join([s for s, v in rule_conf['state'].items() if v == 'enable'])
@@ -244,8 +251,9 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'log' in rule_conf and rule_conf['log'] == 'enable':
action = rule_conf['action'] if 'action' in rule_conf else 'accept'
- output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"')
-
+ #output.append(f'log prefix "[{fw_name[:19]}-{rule_id}-{action[:1].upper()}]"')
+ output.append(f'log prefix "[{family}-{hook}-{fw_name}-{rule_id}-{action[:1].upper()}]"')
+ ##{family}-{hook}-{fw_name}-{rule_id}
if 'log_options' in rule_conf:
if 'level' in rule_conf['log_options']:
@@ -379,6 +387,13 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
conn_mark_str = ','.join(rule_conf['connection_mark'])
output.append(f'ct mark {{{conn_mark_str}}}')
+ if 'vlan' in rule_conf:
+ if 'id' in rule_conf['vlan']:
+ output.append(f'vlan id {rule_conf["vlan"]["id"]}')
+ if 'priority' in rule_conf['vlan']:
+ output.append(f'vlan pcp {rule_conf["vlan"]["priority"]}')
+
+
output.append('counter')
if 'set' in rule_conf:
@@ -404,7 +419,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
else:
output.append('return')
- output.append(f'comment "{hook}-{fw_name}-{rule_id}"')
+ output.append(f'comment "{family}-{hook}-{fw_name}-{rule_id}"')
return " ".join(output)
def parse_tcp_flags(flags):
diff --git a/python/vyos/ifconfig/control.py b/python/vyos/ifconfig/control.py
index c8366cb58..7402da55a 100644
--- a/python/vyos/ifconfig/control.py
+++ b/python/vyos/ifconfig/control.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -49,6 +49,18 @@ class Control(Section):
return popen(command, self.debug)
def _cmd(self, command):
+ import re
+ if 'netns' in self.config:
+ # This command must be executed from default netns 'ip link set dev X netns X'
+ # exclude set netns cmd from netns to avoid:
+ # failed to run command: ip netns exec ns01 ip link set dev veth20 netns ns01
+ pattern = r'ip link set dev (\S+) netns (\S+)'
+ matches = re.search(pattern, command)
+ if matches and matches.group(2) == self.config['netns']:
+ # Command already includes netns and matches desired namespace:
+ command = command
+ else:
+ command = f'ip netns exec {self.config["netns"]} {command}'
return cmd(command, self.debug)
def _get_command(self, config, name):
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 41ce352ad..050095364 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -38,7 +38,9 @@ from vyos.utils.dict import dict_search
from vyos.utils.file import read_file
from vyos.utils.network import get_interface_config
from vyos.utils.network import get_interface_namespace
+from vyos.utils.network import is_netns_interface
from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import run
from vyos.template import is_ipv4
from vyos.template import is_ipv6
from vyos.utils.network import is_intf_addr_assigned
@@ -138,9 +140,6 @@ class Interface(Control):
'validate': assert_mtu,
'shellcmd': 'ip link set dev {ifname} mtu {value}',
},
- 'netns': {
- 'shellcmd': 'ip link set dev {ifname} netns {value}',
- },
'vrf': {
'convert': lambda v: f'master {v}' if v else 'nomaster',
'shellcmd': 'ip link set dev {ifname} {value}',
@@ -175,10 +174,6 @@ class Interface(Control):
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding',
},
- 'rp_filter': {
- 'validate': lambda flt: assert_range(flt,0,3),
- 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter',
- },
'ipv6_accept_ra': {
'validate': lambda ara: assert_range(ara,0,3),
'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
@@ -252,9 +247,6 @@ class Interface(Control):
'ipv4_directed_broadcast': {
'location': '/proc/sys/net/ipv4/conf/{ifname}/bc_forwarding',
},
- 'rp_filter': {
- 'location': '/proc/sys/net/ipv4/conf/{ifname}/rp_filter',
- },
'ipv6_accept_ra': {
'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
},
@@ -286,8 +278,11 @@ class Interface(Control):
}
@classmethod
- def exists(cls, ifname):
- return os.path.exists(f'/sys/class/net/{ifname}')
+ def exists(cls, ifname: str, netns: str=None) -> bool:
+ cmd = f'ip link show dev {ifname}'
+ if netns:
+ cmd = f'ip netns exec {netns} {cmd}'
+ return run(cmd) == 0
@classmethod
def get_config(cls):
@@ -355,7 +350,13 @@ class Interface(Control):
self.vrrp = VRRP(ifname)
def _create(self):
+ # Do not create interface that already exist or exists in netns
+ netns = self.config.get('netns', None)
+ if self.exists(f'{self.ifname}', netns=netns):
+ return
+
cmd = 'ip link add dev {ifname} type {type}'.format(**self.config)
+ if 'netns' in self.config: cmd = f'ip netns exec {netns} {cmd}'
self._cmd(cmd)
def remove(self):
@@ -390,6 +391,9 @@ class Interface(Control):
# after interface removal no other commands should be allowed
# to be called and instead should raise an Exception:
cmd = 'ip link del dev {ifname}'.format(**self.config)
+ # for delete we can't get data from self.config{'netns'}
+ netns = get_interface_namespace(self.ifname)
+ if netns: cmd = f'ip netns exec {netns} {cmd}'
return self._cmd(cmd)
def _set_vrf_ct_zone(self, vrf):
@@ -397,6 +401,10 @@ class Interface(Control):
Add/Remove rules in nftables to associate traffic in VRF to an
individual conntack zone
"""
+ # Don't allow for netns yet
+ if 'netns' in self.config:
+ return None
+
if vrf:
# Get routing table ID for VRF
vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get(
@@ -540,36 +548,30 @@ class Interface(Control):
if prev_state == 'up':
self.set_admin_state('up')
- def del_netns(self, netns):
- """
- Remove interface from given NETNS.
- """
-
- # If NETNS does not exist then there is nothing to delete
+ def del_netns(self, netns: str) -> bool:
+ """ Remove interface from given network namespace """
+ # If network namespace does not exist then there is nothing to delete
if not os.path.exists(f'/run/netns/{netns}'):
- return None
-
- # As a PoC we only allow 'dummy' interfaces
- if 'dum' not in self.ifname:
- return None
+ return False
- # Check if interface realy exists in namespace
- if get_interface_namespace(self.ifname) != None:
- self._cmd(f'ip netns exec {get_interface_namespace(self.ifname)} ip link del dev {self.ifname}')
- return
+ # Check if interface exists in network namespace
+ if is_netns_interface(self.ifname, netns):
+ self._cmd(f'ip netns exec {netns} ip link del dev {self.ifname}')
+ return True
+ return False
- def set_netns(self, netns):
+ def set_netns(self, netns: str) -> bool:
"""
- Add interface from given NETNS.
+ Add interface from given network namespace
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('dum0').set_netns('foo')
"""
+ self._cmd(f'ip link set dev {self.ifname} netns {netns}')
+ return True
- self.set_interface('netns', netns)
-
- def set_vrf(self, vrf):
+ def set_vrf(self, vrf: str) -> bool:
"""
Add/Remove interface from given VRF instance.
@@ -581,10 +583,11 @@ class Interface(Control):
tmp = self.get_interface('vrf')
if tmp == vrf:
- return None
+ return False
self.set_interface('vrf', vrf)
self._set_vrf_ct_zone(vrf)
+ return True
def set_arp_cache_tmo(self, tmo):
"""
@@ -621,6 +624,10 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_tcp_ipv4_mss(1340)
"""
+ # Don't allow for netns yet
+ if 'netns' in self.config:
+ return None
+
self._cleanup_mss_rules('raw', self.ifname)
nft_prefix = 'nft add rule raw VYOS_TCP_MSS'
base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn'
@@ -641,6 +648,10 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_tcp_mss(1320)
"""
+ # Don't allow for netns yet
+ if 'netns' in self.config:
+ return None
+
self._cleanup_mss_rules('ip6 raw', self.ifname)
nft_prefix = 'nft add rule ip6 raw VYOS_TCP_MSS'
base_cmd = f'oifname "{self.ifname}" tcp flags & (syn|rst) == syn'
@@ -745,40 +756,36 @@ class Interface(Control):
return None
return self.set_interface('ipv4_directed_broadcast', forwarding)
- def set_ipv4_source_validation(self, value):
- """
- Help prevent attacks used by Spoofing IP Addresses. Reverse path
- filtering is a Kernel feature that, when enabled, is designed to ensure
- packets that are not routable to be dropped. The easiest example of this
- would be and IP Address of the range 10.0.0.0/8, a private IP Address,
- being received on the Internet facing interface of the router.
+ def _cleanup_ipv4_source_validation_rules(self, ifname):
+ results = self._cmd(f'nft -a list chain ip raw vyos_rpfilter').split("\n")
+ for line in results:
+ if f'iifname "{ifname}"' in line:
+ handle_search = re.search('handle (\d+)', line)
+ if handle_search:
+ self._cmd(f'nft delete rule ip raw vyos_rpfilter handle {handle_search[1]}')
- As per RFC3074.
+ def set_ipv4_source_validation(self, mode):
"""
- if value == 'strict':
- value = 1
- elif value == 'loose':
- value = 2
- else:
- value = 0
-
- all_rp_filter = int(read_file('/proc/sys/net/ipv4/conf/all/rp_filter'))
- if all_rp_filter > value:
- global_setting = 'disable'
- if all_rp_filter == 1: global_setting = 'strict'
- elif all_rp_filter == 2: global_setting = 'loose'
-
- from vyos.base import Warning
- Warning(f'Global source-validation is set to "{global_setting}", this '\
- f'overrides per interface setting on "{self.ifname}"!')
+ Set IPv4 reverse path validation
- tmp = self.get_interface('rp_filter')
- if int(tmp) == value:
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('eth0').set_ipv4_source_validation('strict')
+ """
+ # Don't allow for netns yet
+ if 'netns' in self.config:
return None
- return self.set_interface('rp_filter', value)
+
+ self._cleanup_ipv4_source_validation_rules(self.ifname)
+ nft_prefix = f'nft insert rule ip raw vyos_rpfilter iifname "{self.ifname}"'
+ if mode in ['strict', 'loose']:
+ self._cmd(f"{nft_prefix} counter return")
+ if mode == 'strict':
+ self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop")
+ elif mode == 'loose':
+ self._cmd(f"{nft_prefix} fib saddr oif 0 counter drop")
def _cleanup_ipv6_source_validation_rules(self, ifname):
- commands = []
results = self._cmd(f'nft -a list chain ip6 raw vyos_rpfilter').split("\n")
for line in results:
if f'iifname "{ifname}"' in line:
@@ -794,8 +801,14 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_ipv6_source_validation('strict')
"""
+ # Don't allow for netns yet
+ if 'netns' in self.config:
+ return None
+
self._cleanup_ipv6_source_validation_rules(self.ifname)
- nft_prefix = f'nft add rule ip6 raw vyos_rpfilter iifname "{self.ifname}"'
+ nft_prefix = f'nft insert rule ip6 raw vyos_rpfilter iifname "{self.ifname}"'
+ if mode in ['strict', 'loose']:
+ self._cmd(f"{nft_prefix} counter return")
if mode == 'strict':
self._cmd(f"{nft_prefix} fib saddr . iif oif 0 counter drop")
elif mode == 'loose':
@@ -1143,13 +1156,17 @@ class Interface(Control):
if addr in self._addr:
return False
+ # get interface network namespace if specified
+ netns = self.config.get('netns', None)
+
# add to interface
if addr == 'dhcp':
self.set_dhcp(True)
elif addr == 'dhcpv6':
self.set_dhcpv6(True)
- elif not is_intf_addr_assigned(self.ifname, addr):
- tmp = f'ip addr add {addr} dev {self.ifname}'
+ elif not is_intf_addr_assigned(self.ifname, addr, netns=netns):
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+ tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}'
# Add broadcast address for IPv4
if is_ipv4(addr): tmp += ' brd +'
@@ -1189,13 +1206,17 @@ class Interface(Control):
if not addr:
raise ValueError()
+ # get interface network namespace if specified
+ netns = self.config.get('netns', None)
+
# remove from interface
if addr == 'dhcp':
self.set_dhcp(False)
elif addr == 'dhcpv6':
self.set_dhcpv6(False)
- elif is_intf_addr_assigned(self.ifname, addr):
- self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
+ elif is_intf_addr_assigned(self.ifname, addr, netns=netns):
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+ self._cmd(f'{netns_cmd} ip addr del {addr} dev {self.ifname}')
else:
return False
@@ -1215,8 +1236,11 @@ class Interface(Control):
self.set_dhcp(False)
self.set_dhcpv6(False)
+ netns = get_interface_namespace(self.ifname)
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+ cmd = f'{netns_cmd} ip addr flush dev {self.ifname}'
# flush all addresses
- self._cmd(f'ip addr flush dev "{self.ifname}"')
+ self._cmd(cmd)
def add_to_bridge(self, bridge_dict):
"""
@@ -1371,6 +1395,11 @@ class Interface(Control):
# - https://man7.org/linux/man-pages/man8/tc-mirred.8.html
# Depening if we are the source or the target interface of the port
# mirror we need to setup some variables.
+
+ # Don't allow for netns yet
+ if 'netns' in self.config:
+ return None
+
source_if = self.config['ifname']
mirror_config = None
@@ -1471,8 +1500,8 @@ class Interface(Control):
# Since the interface is pushed onto a separate logical stack
# Configure NETNS
if dict_search('netns', config) != None:
- self.set_netns(config.get('netns', ''))
- return
+ if not is_netns_interface(self.ifname, self.config['netns']):
+ self.set_netns(config.get('netns', ''))
else:
self.del_netns(config.get('netns', ''))
diff --git a/python/vyos/ifconfig/vxlan.py b/python/vyos/ifconfig/vxlan.py
index 6a9911588..1fe5db7cd 100644
--- a/python/vyos/ifconfig/vxlan.py
+++ b/python/vyos/ifconfig/vxlan.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -13,9 +13,15 @@
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+from json import loads
+
from vyos import ConfigError
+from vyos.configdict import list_diff
from vyos.ifconfig import Interface
+from vyos.utils.assertion import assert_list
from vyos.utils.dict import dict_search
+from vyos.utils.network import get_interface_config
+from vyos.utils.network import get_vxlan_vlan_tunnels
@Interface.register
class VXLANIf(Interface):
@@ -49,6 +55,13 @@ class VXLANIf(Interface):
}
}
+ _command_set = {**Interface._command_set, **{
+ 'vlan_tunnel': {
+ 'validate': lambda v: assert_list(v, ['on', 'off']),
+ 'shellcmd': 'bridge link set dev {ifname} vlan_tunnel {value}',
+ },
+ }}
+
def _create(self):
# This table represents a mapping from VyOS internal config dict to
# arguments used by iproute2. For more information please refer to:
@@ -99,3 +112,54 @@ class VXLANIf(Interface):
cmd = f'bridge fdb append to 00:00:00:00:00:00 dst {remote} ' \
'port {port} dev {ifname}'
self._cmd(cmd.format(**self.config))
+
+ def set_vlan_vni_mapping(self, state):
+ """
+ Controls whether vlan to tunnel mapping is enabled on the port.
+ By default this flag is off.
+ """
+ if not isinstance(state, bool):
+ raise ValueError('Value out of range')
+
+ cur_vlan_ids = []
+ if 'vlan_to_vni_removed' in self.config:
+ cur_vlan_ids = self.config['vlan_to_vni_removed']
+ for vlan in cur_vlan_ids:
+ self._cmd(f'bridge vlan del dev {self.ifname} vid {vlan}')
+
+ # Determine current OS Kernel vlan_tunnel setting - only adjust when needed
+ tmp = get_interface_config(self.ifname)
+ cur_state = 'on' if dict_search(f'linkinfo.info_slave_data.vlan_tunnel', tmp) == True else 'off'
+ new_state = 'on' if state else 'off'
+ if cur_state != new_state:
+ self.set_interface('vlan_tunnel', new_state)
+
+ # Determine current OS Kernel configured VLANs
+ os_configured_vlan_ids = get_vxlan_vlan_tunnels(self.ifname)
+
+ if 'vlan_to_vni' in self.config:
+ add_vlan = list_diff(list(self.config['vlan_to_vni'].keys()), os_configured_vlan_ids)
+
+ for vlan, vlan_config in self.config['vlan_to_vni'].items():
+ # VLAN mapping already exists - skip
+ if vlan not in add_vlan:
+ continue
+
+ vni = vlan_config['vni']
+ # The following commands must be run one after another,
+ # they can not be combined with linux 6.1 and iproute2 6.1
+ self._cmd(f'bridge vlan add dev {self.ifname} vid {vlan}')
+ self._cmd(f'bridge vlan add dev {self.ifname} vid {vlan} tunnel_info id {vni}')
+
+ def update(self, config):
+ """ General helper function which works on a dictionary retrived by
+ get_config_dict(). It's main intention is to consolidate the scattered
+ interface setup code and provide a single point of entry when workin
+ on any interface. """
+
+ # call base class last
+ super().update(config)
+
+ # Enable/Disable VLAN tunnel mapping
+ # This is only possible after the interface was assigned to the bridge
+ self.set_vlan_vni_mapping(dict_search('vlan_to_vni', config) != None)
diff --git a/python/vyos/template.py b/python/vyos/template.py
index e167488c6..c1b57b883 100644
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -663,6 +663,84 @@ def nat_static_rule(rule_conf, rule_id, nat_type):
from vyos.nat import parse_nat_static_rule
return parse_nat_static_rule(rule_conf, rule_id, nat_type)
+@register_filter('conntrack_ignore_rule')
+def conntrack_ignore_rule(rule_conf, rule_id, ipv6=False):
+ ip_prefix = 'ip6' if ipv6 else 'ip'
+ def_suffix = '6' if ipv6 else ''
+ output = []
+
+ if 'inbound_interface' in rule_conf:
+ ifname = rule_conf['inbound_interface']
+ output.append(f'iifname {ifname}')
+
+ if 'protocol' in rule_conf:
+ proto = rule_conf['protocol']
+ output.append(f'meta l4proto {proto}')
+
+ for side in ['source', 'destination']:
+ if side in rule_conf:
+ side_conf = rule_conf[side]
+ prefix = side[0]
+
+ if 'address' in side_conf:
+ address = side_conf['address']
+ operator = ''
+ if address[0] == '!':
+ operator = '!='
+ address = address[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} {address}')
+
+ if 'port' in side_conf:
+ port = side_conf['port']
+ operator = ''
+ if port[0] == '!':
+ operator = '!='
+ port = port[1:]
+ output.append(f'th {prefix}port {operator} {port}')
+
+ if 'group' in side_conf:
+ group = side_conf['group']
+
+ if 'address_group' in group:
+ group_name = group['address_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} @A{def_suffix}_{group_name}')
+ # Generate firewall group domain-group
+ elif 'domain_group' in group:
+ group_name = group['domain_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} @D_{group_name}')
+ elif 'network_group' in group:
+ group_name = group['network_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_prefix} {prefix}addr {operator} @N{def_suffix}_{group_name}')
+ if 'port_group' in group:
+ group_name = group['port_group']
+
+ if proto == 'tcp_udp':
+ proto = 'th'
+
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+
+ output.append(f'{proto} {prefix}port {operator} @P_{group_name}')
+
+ output.append('counter notrack')
+ output.append(f'comment "ignore-{rule_id}"')
+
+ return " ".join(output)
+
@register_filter('range_to_regex')
def range_to_regex(num_range):
"""Convert range of numbers or list of ranges
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index 2f181d8d9..4c579c760 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -40,13 +40,19 @@ def interface_exists(interface) -> bool:
import os
return os.path.exists(f'/sys/class/net/{interface}')
-def interface_exists_in_netns(interface_name, netns):
+def is_netns_interface(interface, netns):
from vyos.utils.process import rc_cmd
- rc, out = rc_cmd(f'ip netns exec {netns} ip link show dev {interface_name}')
+ rc, out = rc_cmd(f'sudo ip netns exec {netns} ip link show dev {interface}')
if rc == 0:
return True
return False
+def get_netns_all() -> list:
+ from json import loads
+ from vyos.utils.process import cmd
+ tmp = loads(cmd('ip --json netns ls'))
+ return [ netns['name'] for netns in tmp ]
+
def get_vrf_members(vrf: str) -> list:
"""
Get list of interface VRF members
@@ -78,8 +84,7 @@ def get_interface_config(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
"""
- import os
- if not os.path.exists(f'/sys/class/net/{interface}'):
+ if not interface_exists(interface):
return None
from json import loads
from vyos.utils.process import cmd
@@ -90,33 +95,63 @@ def get_interface_address(interface):
""" Returns the used encapsulation protocol for given interface.
If interface does not exist, None is returned.
"""
- import os
- if not os.path.exists(f'/sys/class/net/{interface}'):
+ if not interface_exists(interface):
return None
from json import loads
from vyos.utils.process import cmd
tmp = loads(cmd(f'ip --detail --json addr show dev {interface}'))[0]
return tmp
-def get_interface_namespace(iface):
+def get_interface_namespace(interface: str):
"""
Returns wich netns the interface belongs to
"""
from json import loads
from vyos.utils.process import cmd
- # Check if netns exist
- tmp = loads(cmd(f'ip --json netns ls'))
- if len(tmp) == 0:
- return None
- for ns in tmp:
+ # Bail out early if netns does not exist
+ tmp = cmd(f'ip --json netns ls')
+ if not tmp: return None
+
+ for ns in loads(tmp):
netns = f'{ns["name"]}'
# Search interface in each netns
data = loads(cmd(f'ip netns exec {netns} ip --json link show'))
for tmp in data:
- if iface == tmp["ifname"]:
+ if interface == tmp["ifname"]:
return netns
+def is_ipv6_tentative(iface: str, ipv6_address: str) -> bool:
+ """Check if IPv6 address is in tentative state.
+
+ This function checks if an IPv6 address on a specific network interface is
+ in the tentative state. IPv6 tentative addresses are not fully configured
+ and are undergoing Duplicate Address Detection (DAD) to ensure they are
+ unique on the network.
+
+ Args:
+ iface (str): The name of the network interface.
+ ipv6_address (str): The IPv6 address to check.
+
+ Returns:
+ bool: True if the IPv6 address is tentative, False otherwise.
+ """
+ import json
+ from vyos.utils.process import rc_cmd
+
+ rc, out = rc_cmd(f'ip -6 --json address show dev {iface} scope global')
+ if rc:
+ return False
+
+ data = json.loads(out)
+ for addr_info in data[0]['addr_info']:
+ if (
+ addr_info.get('local') == ipv6_address and
+ addr_info.get('tentative', False)
+ ):
+ return True
+ return False
+
def is_wwan_connected(interface):
""" Determine if a given WWAN interface, e.g. wwan0 is connected to the
carrier network or not """
@@ -141,8 +176,7 @@ def is_wwan_connected(interface):
def get_bridge_fdb(interface):
""" Returns the forwarding database entries for a given interface """
- import os
- if not os.path.exists(f'/sys/class/net/{interface}'):
+ if not interface_exists(interface):
return None
from json import loads
from vyos.utils.process import cmd
@@ -274,57 +308,33 @@ def is_addr_assigned(ip_address, vrf=None) -> bool:
return False
-def is_intf_addr_assigned(intf, address) -> bool:
+def is_intf_addr_assigned(ifname: str, addr: str, netns: str=None) -> bool:
"""
Verify if the given IPv4/IPv6 address is assigned to specific interface.
It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
address 192.0.2.1/24.
"""
- from vyos.template import is_ipv4
-
- from netifaces import ifaddresses
- from netifaces import AF_INET
- from netifaces import AF_INET6
-
- # check if the requested address type is configured at all
- # {
- # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
- # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}],
- # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}]
- # }
- try:
- addresses = ifaddresses(intf)
- except ValueError as e:
- print(e)
- return False
-
- # determine IP version (AF_INET or AF_INET6) depending on passed address
- addr_type = AF_INET if is_ipv4(address) else AF_INET6
-
- # Check every IP address on this interface for a match
- netmask = None
- if '/' in address:
- address, netmask = address.split('/')
- for ip in addresses.get(addr_type, []):
- # ip can have the interface name in the 'addr' field, we need to remove it
- # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'}
- ip_addr = ip['addr'].split('%')[0]
-
- if not _are_same_ip(address, ip_addr):
- continue
-
- # we do not have a netmask to compare against, they are the same
- if not netmask:
- return True
+ import json
+ import jmespath
- prefixlen = ''
- if is_ipv4(ip_addr):
- prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')])
- else:
- prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split('/')[0].split(':') if _])
+ from vyos.utils.process import rc_cmd
+ from ipaddress import ip_interface
- if str(prefixlen) == netmask:
- return True
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+ rc, out = rc_cmd(f'{netns_cmd} ip --json address show dev {ifname}')
+ if rc == 0:
+ json_out = json.loads(out)
+ addresses = jmespath.search("[].addr_info[].{family: family, address: local, prefixlen: prefixlen}", json_out)
+ for address_info in addresses:
+ family = address_info['family']
+ address = address_info['address']
+ prefixlen = address_info['prefixlen']
+ # Remove the interface name if present in the given address
+ if '%' in addr:
+ addr = addr.split('%')[0]
+ interface = ip_interface(f"{address}/{prefixlen}")
+ if ip_interface(addr) == interface or address == addr:
+ return True
return False
@@ -398,7 +408,7 @@ def is_subnet_connected(subnet, primary=False):
return False
-def is_afi_configured(interface, afi):
+def is_afi_configured(interface: str, afi):
""" Check if given address family is configured, or in other words - an IP
address is assigned to the interface. """
from netifaces import ifaddresses
@@ -415,3 +425,46 @@ def is_afi_configured(interface, afi):
return False
return afi in addresses
+
+def get_vxlan_vlan_tunnels(interface: str) -> list:
+ """ Return a list of strings with VLAN IDs configured in the Kernel """
+ from json import loads
+ from vyos.utils.process import cmd
+
+ if not interface.startswith('vxlan'):
+ raise ValueError('Only applicable for VXLAN interfaces!')
+
+ # Determine current OS Kernel configured VLANs
+ #
+ # $ bridge -j -p vlan tunnelshow dev vxlan0
+ # [ {
+ # "ifname": "vxlan0",
+ # "tunnels": [ {
+ # "vlan": 10,
+ # "vlanEnd": 11,
+ # "tunid": 10010,
+ # "tunidEnd": 10011
+ # },{
+ # "vlan": 20,
+ # "tunid": 10020
+ # } ]
+ # } ]
+ #
+ os_configured_vlan_ids = []
+ tmp = loads(cmd(f'bridge --json vlan tunnelshow dev {interface}'))
+ if tmp:
+ for tunnel in tmp[0].get('tunnels', {}):
+ vlanStart = tunnel['vlan']
+ if 'vlanEnd' in tunnel:
+ vlanEnd = tunnel['vlanEnd']
+ # Build a real list for user VLAN IDs
+ vlan_list = list(range(vlanStart, vlanEnd +1))
+ # Convert list of integers to list or strings
+ os_configured_vlan_ids.extend(map(str, vlan_list))
+ # Proceed with next tunnel - this one is complete
+ continue
+
+ # Add single tunel id - not part of a range
+ os_configured_vlan_ids.append(str(vlanStart))
+
+ return os_configured_vlan_ids
diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py
index e09c7d86d..9ecdddf09 100644
--- a/python/vyos/utils/process.py
+++ b/python/vyos/utils/process.py
@@ -139,7 +139,7 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
expect: a list of error codes to consider as normal
"""
decoded, code = popen(
- command, flag,
+ command.lstrip(), flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,
@@ -170,7 +170,7 @@ def rc_cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
(1, 'Device "eth99" does not exist.')
"""
out, code = popen(
- command, flag,
+ command.lstrip(), flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,