summaryrefslogtreecommitdiff
path: root/python/vyos/ifconfig/interface.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/vyos/ifconfig/interface.py')
-rwxr-xr-xpython/vyos/ifconfig/interface.py252
1 files changed, 147 insertions, 105 deletions
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 58d130ef6..22441d1d2 100755
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1,4 +1,4 @@
-# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -13,7 +13,6 @@
# 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 netifaces import interfaces
import os
import re
import json
@@ -37,6 +36,7 @@ from vyos.util import mac2eui64
from vyos.util import dict_search
from vyos.util import read_file
from vyos.util import get_interface_config
+from vyos.util import get_interface_namespace
from vyos.util import is_systemd_service_active
from vyos.template import is_ipv4
from vyos.template import is_ipv6
@@ -135,6 +135,9 @@ 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}',
@@ -512,6 +515,35 @@ 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
+ 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
+
+ # 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
+
+ def set_netns(self, netns):
+ """
+ Add interface from given NETNS.
+
+ Example:
+ >>> from vyos.ifconfig import Interface
+ >>> Interface('dum0').set_netns('foo')
+ """
+
+ self.set_interface('netns', netns)
+
def set_vrf(self, vrf):
"""
Add/Remove interface from given VRF instance.
@@ -544,6 +576,15 @@ class Interface(Control):
return None
return self.set_interface('arp_cache_tmo', tmo)
+ def _cleanup_mss_rules(self, table, ifname):
+ commands = []
+ results = self._cmd(f'nft -a list chain {table} VYOS_TCP_MSS').split("\n")
+ for line in results:
+ if f'oifname "{ifname}"' in line:
+ handle_search = re.search('handle (\d+)', line)
+ if handle_search:
+ self._cmd(f'nft delete rule {table} VYOS_TCP_MSS handle {handle_search[1]}')
+
def set_tcp_ipv4_mss(self, mss):
"""
Set IPv4 TCP MSS value advertised when TCP SYN packets leave this
@@ -555,22 +596,14 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_tcp_ipv4_mss(1340)
"""
- iptables_bin = 'iptables'
- base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN'
- out = self._cmd(f'{iptables_bin}-save -t mangle')
- for line in out.splitlines():
- if line.startswith(base_options):
- # remove OLD MSS mangling configuration
- line = line.replace('-A FORWARD', '-D FORWARD')
- self._cmd(f'{iptables_bin} -t mangle {line}')
-
- cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS'
+ 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'
if mss == 'clamp-mss-to-pmtu':
- self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu')
+ self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'")
elif int(mss) > 0:
- # probably add option to clamp only if bigger:
low_mss = str(int(mss) + 1)
- self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}')
+ self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'")
def set_tcp_ipv6_mss(self, mss):
"""
@@ -583,22 +616,14 @@ class Interface(Control):
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_tcp_mss(1320)
"""
- iptables_bin = 'ip6tables'
- base_options = f'-A FORWARD -o {self.ifname} -p tcp -m tcp --tcp-flags SYN,RST SYN'
- out = self._cmd(f'{iptables_bin}-save -t mangle')
- for line in out.splitlines():
- if line.startswith(base_options):
- # remove OLD MSS mangling configuration
- line = line.replace('-A FORWARD', '-D FORWARD')
- self._cmd(f'{iptables_bin} -t mangle {line}')
-
- cmd_mss = f'{iptables_bin} -t mangle {base_options} --jump TCPMSS'
+ 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'
if mss == 'clamp-mss-to-pmtu':
- self._cmd(f'{cmd_mss} --clamp-mss-to-pmtu')
+ self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size set rt mtu'")
elif int(mss) > 0:
- # probably add option to clamp only if bigger:
low_mss = str(int(mss) + 1)
- self._cmd(f'{cmd_mss} -m tcpmss --mss {low_mss}:65535 --set-mss {mss}')
+ self._cmd(f"{nft_prefix} '{base_cmd} tcp option maxseg size {low_mss}-65535 tcp option maxseg size set {mss}'")
def set_arp_filter(self, arp_filter):
"""
@@ -711,8 +736,9 @@ class Interface(Control):
if all_rp_filter == 1: global_setting = 'strict'
elif all_rp_filter == 2: global_setting = 'loose'
- print(f'WARNING: Global source-validation is set to "{global_setting}\n"' \
- 'this overrides per interface setting!')
+ from vyos.base import Warning
+ Warning(f'Global source-validation is set to "{global_setting} '\
+ f'this overrides per interface setting!')
tmp = self.get_interface('rp_filter')
if int(tmp) == value:
@@ -1054,16 +1080,17 @@ class Interface(Control):
if addr in self._addr:
return False
- addr_is_v4 = is_ipv4(addr)
-
# 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):
- self._cmd(f'ip addr add "{addr}" '
- f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"')
+ tmp = f'ip addr add {addr} dev {self.ifname}'
+ # Add broadcast address for IPv4
+ if is_ipv4(addr): tmp += ' brd +'
+
+ self._cmd(tmp)
else:
return False
@@ -1202,12 +1229,11 @@ class Interface(Control):
options_file = f'{config_base}_{ifname}.options'
pid_file = f'{config_base}_{ifname}.pid'
lease_file = f'{config_base}_{ifname}.leases'
-
- # Stop client with old config files to get the right IF_METRIC.
systemd_service = f'dhclient@{ifname}.service'
- if is_systemd_service_active(systemd_service):
- self._cmd(f'systemctl stop {systemd_service}')
+ # 'up' check is mandatory b/c even if the interface is A/D, as soon as
+ # the DHCP client is started the interface will be placed in u/u state.
+ # This is not what we intended to do when disabling an interface.
if enable and 'disable' not in self._config:
if dict_search('dhcp_options.host_name', self._config) == None:
# read configured system hostname.
@@ -1218,16 +1244,19 @@ class Interface(Control):
tmp = {'dhcp_options' : { 'host_name' : hostname}}
self._config = dict_merge(tmp, self._config)
- render(options_file, 'dhcp-client/daemon-options.tmpl',
- self._config)
- render(config_file, 'dhcp-client/ipv4.tmpl',
- self._config)
+ render(options_file, 'dhcp-client/daemon-options.j2', self._config)
+ render(config_file, 'dhcp-client/ipv4.j2', self._config)
- # 'up' check is mandatory b/c even if the interface is A/D, as soon as
- # the DHCP client is started the interface will be placed in u/u state.
- # This is not what we intended to do when disabling an interface.
- return self._cmd(f'systemctl restart {systemd_service}')
+ # When the DHCP client is restarted a brief outage will occur, as
+ # the old lease is released a new one is acquired (T4203). We will
+ # only restart DHCP client if it's option changed, or if it's not
+ # running, but it should be running (e.g. on system startup)
+ if 'dhcp_options_changed' in self._config or not is_systemd_service_active(systemd_service):
+ return self._cmd(f'systemctl restart {systemd_service}')
+ return None
else:
+ if is_systemd_service_active(systemd_service):
+ self._cmd(f'systemctl stop {systemd_service}')
# cleanup old config files
for file in [config_file, options_file, pid_file, lease_file]:
if os.path.isfile(file):
@@ -1246,8 +1275,7 @@ class Interface(Control):
systemd_service = f'dhcp6c@{ifname}.service'
if enable and 'disable' not in self._config:
- render(config_file, 'dhcp-client/ipv6.tmpl',
- self._config)
+ render(config_file, 'dhcp-client/ipv6.j2', self._config)
# We must ignore any return codes. This is required to enable
# DHCPv6-PD for interfaces which are yet not up and running.
@@ -1258,48 +1286,57 @@ class Interface(Control):
if os.path.isfile(config_file):
os.remove(config_file)
- def set_mirror(self):
+ def set_mirror_redirect(self):
# Please refer to the document for details
# - https://man7.org/linux/man-pages/man8/tc.8.html
# - 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.
source_if = self._config['ifname']
- config = self._config.get('mirror', None)
+ mirror_config = None
+ if 'mirror' in self._config:
+ mirror_config = self._config['mirror']
if 'is_mirror_intf' in self._config:
source_if = next(iter(self._config['is_mirror_intf']))
- config = self._config['is_mirror_intf'][source_if].get('mirror', None)
-
- # Check configuration stored by old perl code before delete T3782
- if not 'redirect' in self._config:
- # Please do not clear the 'set $? = 0 '. It's meant to force a return of 0
- # Remove existing mirroring rules
- delete_tc_cmd = f'tc qdisc del dev {source_if} handle ffff: ingress 2> /dev/null;'
- delete_tc_cmd += f'tc qdisc del dev {source_if} handle 1: root prio 2> /dev/null;'
- delete_tc_cmd += 'set $?=0'
- self._popen(delete_tc_cmd)
-
- # Bail out early if nothing needs to be configured
- if not config:
- return
-
- for direction, mirror_if in config.items():
- if mirror_if not in interfaces():
- continue
-
- if direction == 'ingress':
- handle = 'ffff: ingress'
- parent = 'ffff:'
- elif direction == 'egress':
- handle = '1: root prio'
- parent = '1:'
-
- # Mirror egress traffic
- mirror_cmd = f'tc qdisc add dev {source_if} handle {handle}; '
- # Export the mirrored traffic to the interface
- mirror_cmd += f'tc filter add dev {source_if} parent {parent} protocol all prio 10 u32 match u32 0 0 flowid 1:1 action mirred egress mirror dev {mirror_if}'
- self._popen(mirror_cmd)
+ mirror_config = self._config['is_mirror_intf'][source_if].get('mirror', None)
+
+ redirect_config = None
+
+ # clear existing ingess - ignore errors (e.g. "Error: Cannot find specified
+ # qdisc on specified device") - we simply cleanup all stuff here
+ self._popen(f'tc qdisc del dev {source_if} parent ffff: 2>/dev/null');
+ self._popen(f'tc qdisc del dev {source_if} parent 1: 2>/dev/null');
+
+ # Apply interface mirror policy
+ if mirror_config:
+ for direction, target_if in mirror_config.items():
+ if direction == 'ingress':
+ handle = 'ffff: ingress'
+ parent = 'ffff:'
+ elif direction == 'egress':
+ handle = '1: root prio'
+ parent = '1:'
+
+ # Mirror egress traffic
+ mirror_cmd = f'tc qdisc add dev {source_if} handle {handle}; '
+ # Export the mirrored traffic to the interface
+ mirror_cmd += f'tc filter add dev {source_if} parent {parent} protocol '\
+ f'all prio 10 u32 match u32 0 0 flowid 1:1 action mirred '\
+ f'egress mirror dev {target_if}'
+ _, err = self._popen(mirror_cmd)
+ if err: print('tc qdisc(filter for mirror port failed')
+
+ # Apply interface traffic redirection policy
+ elif 'redirect' in self._config:
+ _, err = self._popen(f'tc qdisc add dev {source_if} handle ffff: ingress')
+ if err: print(f'tc qdisc add for redirect failed!')
+
+ target_if = self._config['redirect']
+ _, err = self._popen(f'tc filter add dev {source_if} parent ffff: protocol '\
+ f'all prio 10 u32 match u32 0 0 flowid 1:1 action mirred '\
+ f'egress redirect dev {target_if}')
+ if err: print('tc filter add for redirect failed')
def set_xdp(self, state):
"""
@@ -1353,6 +1390,16 @@ class Interface(Control):
if mac:
self.set_mac(mac)
+ # If interface is connected to NETNS we don't have to check all other
+ # settings like MTU/IPv6/sysctl values, etc.
+ # 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
+ else:
+ self.del_netns(config.get('netns', ''))
+
# Update interface description
self.set_alias(config.get('description', ''))
@@ -1388,9 +1435,6 @@ class Interface(Control):
else:
self.del_addr(addr)
- for addr in new_addr:
- self.add_addr(addr)
-
# start DHCPv6 client when only PD was configured
if dhcpv6pd:
self.set_dhcpv6(True)
@@ -1405,16 +1449,15 @@ class Interface(Control):
# checked before
self.set_vrf(config.get('vrf', ''))
+ # Add this section after vrf T4331
+ for addr in new_addr:
+ self.add_addr(addr)
+
# Configure MSS value for IPv4 TCP connections
tmp = dict_search('ip.adjust_mss', config)
value = tmp if (tmp != None) else '0'
self.set_tcp_ipv4_mss(value)
- # Configure MSS value for IPv6 TCP connections
- tmp = dict_search('ipv6.adjust_mss', config)
- value = tmp if (tmp != None) else '0'
- self.set_tcp_ipv6_mss(value)
-
# Configure ARP cache timeout in milliseconds - has default value
tmp = dict_search('ip.arp_cache_timeout', config)
value = tmp if (tmp != None) else '30'
@@ -1460,6 +1503,18 @@ class Interface(Control):
value = tmp if (tmp != None) else '0'
self.set_ipv4_source_validation(value)
+ # MTU - Maximum Transfer Unit has a default value. It must ALWAYS be set
+ # before mangling any IPv6 option. If MTU is less then 1280 IPv6 will be
+ # automatically disabled by the kernel. Also MTU must be increased before
+ # configuring any IPv6 address on the interface.
+ if 'mtu' in config:
+ self.set_mtu(config.get('mtu'))
+
+ # Configure MSS value for IPv6 TCP connections
+ tmp = dict_search('ipv6.adjust_mss', config)
+ value = tmp if (tmp != None) else '0'
+ self.set_tcp_ipv6_mss(value)
+
# IPv6 forwarding
tmp = dict_search('ipv6.disable_forwarding', config)
value = '0' if (tmp != None) else '1'
@@ -1482,10 +1537,6 @@ class Interface(Control):
value = tmp if (tmp != None) else '1'
self.set_ipv6_dad_messages(value)
- # MTU - Maximum Transfer Unit
- if 'mtu' in config:
- self.set_mtu(config.get('mtu'))
-
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in (dict_search('ipv6.address.eui64_old', config) or []):
self.del_ipv6_eui64_address(addr)
@@ -1510,8 +1561,8 @@ class Interface(Control):
# eXpress Data Path - highly experimental
self.set_xdp('xdp' in config)
- # configure port mirror
- self.set_mirror()
+ # configure interface mirror or redirection target
+ self.set_mirror_redirect()
# Enable/Disable of an interface must always be done at the end of the
# derived class to make use of the ref-counting set_admin_state()
@@ -1536,12 +1587,10 @@ class Interface(Control):
tmp['source_interface'] = ifname
tmp['vlan_id'] = vif_s_id
- vif_s_ifname = f'{ifname}.{vif_s_id}'
- vif_s_config['ifname'] = vif_s_ifname
-
# It is not possible to change the VLAN encapsulation protocol
# "on-the-fly". For this "quirk" we need to actively delete and
# re-create the VIF-S interface.
+ vif_s_ifname = f'{ifname}.{vif_s_id}'
if self.exists(vif_s_ifname):
cur_cfg = get_interface_config(vif_s_ifname)
protocol = dict_search('linkinfo.info_data.protocol', cur_cfg).lower()
@@ -1563,7 +1612,6 @@ class Interface(Control):
tmp['vlan_id'] = vif_c_id
vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}'
- vif_c_config['ifname'] = vif_c_ifname
c_vlan = VLANIf(vif_c_ifname, **tmp)
c_vlan.update(vif_c_config)
@@ -1574,10 +1622,7 @@ class Interface(Control):
# create/update 802.1q VLAN interfaces
for vif_id, vif_config in config.get('vif', {}).items():
-
vif_ifname = f'{ifname}.{vif_id}'
- vif_config['ifname'] = vif_ifname
-
tmp = deepcopy(VLANIf.get_config())
tmp['source_interface'] = ifname
tmp['vlan_id'] = vif_id
@@ -1670,6 +1715,3 @@ class VLANIf(Interface):
return None
return super().set_admin_state(state)
-
- def set_mirror(self):
- return