summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
Diffstat (limited to 'python')
-rw-r--r--python/vyos/configtree.py9
-rw-r--r--python/vyos/defaults.py10
-rw-r--r--python/vyos/firewall.py70
-rw-r--r--python/vyos/ifconfig/vti.py19
-rw-r--r--python/vyos/utils/serial.py118
-rw-r--r--python/vyos/utils/vti_updown_db.py194
6 files changed, 392 insertions, 28 deletions
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 5775070e2..bd77ab899 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -1,5 +1,5 @@
# configtree -- a standalone VyOS config file manipulation library (Python bindings)
-# Copyright (C) 2018-2022 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 VyOS maintainers and contributors
#
# This library is free software; you can redistribute it and/or modify it under the terms of
# the GNU Lesser General Public License as published by the Free Software Foundation;
@@ -290,7 +290,7 @@ class ConfigTree(object):
else:
return True
- def list_nodes(self, path):
+ def list_nodes(self, path, path_must_exist=True):
check_path(path)
path_str = " ".join(map(str, path)).encode()
@@ -298,7 +298,10 @@ class ConfigTree(object):
res = json.loads(res_json)
if res is None:
- raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ if path_must_exist:
+ raise ConfigTreeError("Path [{}] doesn't exist".format(path_str))
+ else:
+ return []
else:
return res
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9ccd925ce..25ee45391 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -50,3 +50,13 @@ commit_lock = os.path.join(directories['vyos_configdir'], '.lock')
component_version_json = os.path.join(directories['data'], 'component-versions.json')
config_default = os.path.join(directories['data'], 'config.boot.default')
+
+rt_symbolic_names = {
+ # Standard routing tables for Linux & reserved IDs for VyOS
+ 'default': 253, # Confusingly, a final fallthru, not the default.
+ 'main': 254, # The actual global table used by iproute2 unless told otherwise.
+ 'local': 255, # Special kernel loopback table.
+}
+
+rt_global_vrf = rt_symbolic_names['main']
+rt_global_table = rt_symbolic_names['main']
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 40399f481..cac6d2953 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -30,6 +30,9 @@ from vyos.utils.dict import dict_search_args
from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import cmd
from vyos.utils.process import run
+from vyos.utils.network import get_vrf_tableid
+from vyos.defaults import rt_global_table
+from vyos.defaults import rt_global_vrf
# Conntrack
def conntrack_required(conf):
@@ -164,7 +167,10 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if address_mask:
operator = '!=' if exclude else '=='
operator = f'& {address_mask} {operator} '
- output.append(f'{ip_name} {prefix}addr {operator}{suffix}')
+ if is_ipv4(suffix):
+ output.append(f'ip {prefix}addr {operator}{suffix}')
+ else:
+ output.append(f'ip6 {prefix}addr {operator}{suffix}')
if 'fqdn' in side_conf:
fqdn = side_conf['fqdn']
@@ -233,22 +239,38 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'group' in side_conf:
group = side_conf['group']
- if 'address_group' in group:
- group_name = group['address_group']
- operator = ''
- exclude = group_name[0] == "!"
- if exclude:
- operator = '!='
- group_name = group_name[1:]
- if address_mask:
- operator = '!=' if exclude else '=='
- operator = f'& {address_mask} {operator}'
- output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
- elif 'dynamic_address_group' in group:
+ for ipvx_address_group in ['address_group', 'ipv4_address_group', 'ipv6_address_group']:
+ if ipvx_address_group in group:
+ group_name = group[ipvx_address_group]
+ operator = ''
+ exclude = group_name[0] == "!"
+ if exclude:
+ operator = '!='
+ group_name = group_name[1:]
+ if address_mask:
+ operator = '!=' if exclude else '=='
+ operator = f'& {address_mask} {operator}'
+ # for bridge, change ip_name
+ if ip_name == 'bri':
+ ip_name = 'ip' if ipvx_address_group == 'ipv4_address_group' else 'ip6'
+ def_suffix = '6' if ipvx_address_group == 'ipv6_address_group' else ''
+ output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
+ for ipvx_network_group in ['network_group', 'ipv4_network_group', 'ipv6_network_group']:
+ if ipvx_network_group in group:
+ group_name = group[ipvx_network_group]
+ operator = ''
+ if group_name[0] == "!":
+ operator = '!='
+ group_name = group_name[1:]
+ # for bridge, change ip_name
+ if ip_name == 'bri':
+ ip_name = 'ip' if ipvx_network_group == 'ipv4_network_group' else 'ip6'
+ def_suffix = '6' if ipvx_network_group == 'ipv6_network_group' else ''
+ output.append(f'{ip_name} {prefix}addr {operator} @N{def_suffix}_{group_name}')
+ if 'dynamic_address_group' in group:
group_name = group['dynamic_address_group']
operator = ''
- exclude = group_name[0] == "!"
- if exclude:
+ if group_name[0] == "!":
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @DA{def_suffix}_{group_name}')
@@ -260,13 +282,6 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {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_name} {prefix}addr {operator} @N{def_suffix}_{group_name}')
if 'mac_group' in group:
group_name = group['mac_group']
operator = ''
@@ -473,11 +488,20 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
if 'mark' in rule_conf['set']:
mark = rule_conf['set']['mark']
output.append(f'meta mark set {mark}')
+ if 'vrf' in rule_conf['set']:
+ set_table = True
+ vrf_name = rule_conf['set']['vrf']
+ if vrf_name == 'default':
+ table = rt_global_vrf
+ else:
+ # NOTE: VRF->table ID lookup depends on the VRF iface already existing.
+ table = get_vrf_tableid(vrf_name)
if 'table' in rule_conf['set']:
set_table = True
table = rule_conf['set']['table']
if table == 'main':
- table = '254'
+ table = rt_global_table
+ if set_table:
mark = 0x7FFFFFFF - int(table)
output.append(f'meta mark set {mark}')
if 'tcp_mss' in rule_conf['set']:
diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py
index 9511386f4..251cbeb36 100644
--- a/python/vyos/ifconfig/vti.py
+++ b/python/vyos/ifconfig/vti.py
@@ -15,6 +15,7 @@
from vyos.ifconfig.interface import Interface
from vyos.utils.dict import dict_search
+from vyos.utils.vti_updown_db import vti_updown_db_exists, open_vti_updown_db_readonly
@Interface.register
class VTIIf(Interface):
@@ -27,6 +28,10 @@ class VTIIf(Interface):
},
}
+ def __init__(self, ifname, **kwargs):
+ self.bypass_vti_updown_db = kwargs.pop("bypass_vti_updown_db", False)
+ super().__init__(ifname, **kwargs)
+
def _create(self):
# This table represents a mapping from VyOS internal config dict to
# arguments used by iproute2. For more information please refer to:
@@ -57,8 +62,18 @@ class VTIIf(Interface):
self.set_interface('admin_state', 'down')
def set_admin_state(self, state):
- """ Handled outside by /etc/ipsec.d/vti-up-down """
- pass
+ """
+ Set interface administrative state to be 'up' or 'down'.
+
+ The interface will only be brought 'up' if ith is attached to an
+ active ipsec site-to-site connection or remote access connection.
+ """
+ if state == 'down' or self.bypass_vti_updown_db:
+ super().set_admin_state(state)
+ elif vti_updown_db_exists():
+ with open_vti_updown_db_readonly() as db:
+ if db.wantsInterfaceUp(self.ifname):
+ super().set_admin_state(state)
def get_mac(self):
""" Get a synthetic MAC address. """
diff --git a/python/vyos/utils/serial.py b/python/vyos/utils/serial.py
new file mode 100644
index 000000000..b646f881e
--- /dev/null
+++ b/python/vyos/utils/serial.py
@@ -0,0 +1,118 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os, re, json
+from typing import List
+
+from vyos.base import Warning
+from vyos.utils.io import ask_yes_no
+from vyos.utils.process import cmd
+
+GLOB_GETTY_UNITS = 'serial-getty@*.service'
+RE_GETTY_DEVICES = re.compile(r'.+@(.+).service$')
+
+SD_UNIT_PATH = '/run/systemd/system'
+UTMP_PATH = '/run/utmp'
+
+def get_serial_units(include_devices=[]):
+ # Since we cannot depend on the current config for decommissioned ports,
+ # we just grab everything that systemd knows about.
+ tmp = cmd(f'systemctl list-units {GLOB_GETTY_UNITS} --all --output json --no-pager')
+ getty_units = json.loads(tmp)
+ for sdunit in getty_units:
+ m = RE_GETTY_DEVICES.search(sdunit['unit'])
+ if m is None:
+ Warning(f'Serial console unit name "{sdunit["unit"]}" is malformed and cannot be checked for activity!')
+ continue
+
+ getty_device = m.group(1)
+ if include_devices and getty_device not in include_devices:
+ continue
+
+ sdunit['device'] = getty_device
+
+ return getty_units
+
+def get_authenticated_ports(units):
+ connected = []
+ ports = [ x['device'] for x in units if 'device' in x ]
+ #
+ # utmpdump just gives us an easily parseable dump of currently logged-in sessions, for eg:
+ # $ utmpdump /run/utmp
+ # Utmp dump of /run/utmp
+ # [2] [00000] [~~ ] [reboot ] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:56:53,958484+00:00]
+ # [1] [00051] [~~ ] [runlevel] [~ ] [6.6.31-amd64-vyos ] [0.0.0.0 ] [2024-06-18T13:57:01,790808+00:00]
+ # [6] [03178] [tty1] [LOGIN ] [tty1 ] [ ] [0.0.0.0 ] [2024-06-18T13:57:31,015392+00:00]
+ # [7] [37151] [ts/0] [vyos ] [pts/0 ] [10.9.8.7 ] [10.9.8.7 ] [2024-07-04T13:42:08,760892+00:00]
+ # [8] [24812] [ts/1] [ ] [pts/1 ] [10.9.8.7 ] [10.9.8.7 ] [2024-06-20T18:10:07,309365+00:00]
+ #
+ # We can safely skip blank or LOGIN sessions with valid device names.
+ #
+ for line in cmd(f'utmpdump {UTMP_PATH}').splitlines():
+ row = line.split('] [')
+ user_name = row[3].strip()
+ user_term = row[4].strip()
+ if user_name and user_name != 'LOGIN' and user_term in ports:
+ connected.append(user_term)
+
+ return connected
+
+def restart_login_consoles(prompt_user=False, quiet=True, devices: List[str]=[]):
+ # restart_login_consoles() is called from both conf- and op-mode scripts, including
+ # the warning messages and user prompts common to both.
+ #
+ # The default case, called with no arguments, is a simple serial-getty restart &
+ # cleanup wrapper with no output or prompts that can be used from anywhere.
+ #
+ # quiet and prompt_user args have been split from an original "no_prompt", in
+ # order to support the completely silent default use case. "no_prompt" would
+ # only suppress the user interactive prompt.
+ #
+ # quiet intentionally does not suppress a vyos.base.Warning() for malformed
+ # device names in _get_serial_units().
+ #
+ cmd('systemctl daemon-reload')
+
+ units = get_serial_units(devices)
+ connected = get_authenticated_ports(units)
+
+ if connected:
+ if not quiet:
+ Warning('There are user sessions connected via serial console that '\
+ 'will be terminated when serial console settings are changed!')
+ if not prompt_user:
+ # This flag is used by conf_mode/system_console.py to reset things, if there's
+ # a problem, the user should issue a manual restart for serial-getty.
+ Warning('Please ensure all settings are committed and saved before issuing a ' \
+ '"restart serial console" command to apply new configuration!')
+ if not prompt_user:
+ return False
+ if not ask_yes_no('Any uncommitted changes from these sessions will be lost\n' \
+ 'and in-progress actions may be left in an inconsistent state.\n'\
+ '\nContinue?'):
+ return False
+
+ for unit in units:
+ if 'device' not in unit:
+ continue # malformed or filtered.
+ unit_name = unit['unit']
+ unit_device = unit['device']
+ if os.path.exists(os.path.join(SD_UNIT_PATH, unit_name)):
+ cmd(f'systemctl restart {unit_name}')
+ else:
+ # Deleted stubs don't need to be restarted, just shut them down.
+ cmd(f'systemctl stop {unit_name}')
+
+ return True
diff --git a/python/vyos/utils/vti_updown_db.py b/python/vyos/utils/vti_updown_db.py
new file mode 100644
index 000000000..b491fc6f2
--- /dev/null
+++ b/python/vyos/utils/vti_updown_db.py
@@ -0,0 +1,194 @@
+# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from contextlib import contextmanager
+from syslog import syslog
+
+VTI_WANT_UP_IFLIST = '/tmp/ipsec_vti_interfaces'
+
+def vti_updown_db_exists():
+ """ Returns true if the database exists """
+ return os.path.exists(VTI_WANT_UP_IFLIST)
+
+@contextmanager
+def open_vti_updown_db_for_create_or_update():
+ """ Opens the database for reading and writing, creating the database if it does not exist """
+ if vti_updown_db_exists():
+ f = open(VTI_WANT_UP_IFLIST, 'r+')
+ else:
+ f = open(VTI_WANT_UP_IFLIST, 'x+')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+@contextmanager
+def open_vti_updown_db_for_update():
+ """ Opens the database for reading and writing, returning an error if it does not exist """
+ f = open(VTI_WANT_UP_IFLIST, 'r+')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+@contextmanager
+def open_vti_updown_db_readonly():
+ """ Opens the database for reading, returning an error if it does not exist """
+ f = open(VTI_WANT_UP_IFLIST, 'r')
+ try:
+ db = VTIUpDownDB(f)
+ yield db
+ finally:
+ f.close()
+
+def remove_vti_updown_db():
+ """ Brings down any interfaces referenced by the database and removes the database """
+ # We need to process the DB first to bring down any interfaces still up
+ with open_vti_updown_db_for_update() as db:
+ db.removeAllOtherInterfaces([])
+ # this usage of commit will only ever bring down interfaces,
+ # do not need to provide a functional interface dict supplier
+ db.commit(lambda _: None)
+
+ os.unlink(VTI_WANT_UP_IFLIST)
+
+class VTIUpDownDB:
+ # The VTI Up-Down DB is a text-based database of space-separated "ifspecs".
+ #
+ # ifspecs can come in one of the two following formats:
+ #
+ # persistent format: <interface name>
+ # indicates the named interface should always be up.
+ #
+ # connection format: <interface name>:<connection name>:<protocol>
+ # indicates the named interface wants to be up due to an established
+ # connection <connection name> using the <protocol> protocol.
+ #
+ # The configuration tree and ipsec daemon connection up-down hook
+ # modify this file as needed and use it to determine when a
+ # particular event or configuration change should lead to changing
+ # the interface state.
+
+ def __init__(self, f):
+ self._fileHandle = f
+ self._ifspecs = set([entry.strip() for entry in f.read().split(" ") if entry and not entry.isspace()])
+ self._ifsUp = set()
+ self._ifsDown = set()
+
+ def add(self, interface, connection = None, protocol = None):
+ """
+ Adds a new entry to the DB.
+
+ If an interface name, connection name, and protocol are supplied,
+ creates a connection entry.
+
+ If only an interface name is specified, creates a persistent entry
+ for the given interface.
+ """
+ ifspec = f"{interface}:{connection}:{protocol}" if (connection is not None and protocol is not None) else interface
+ if ifspec not in self._ifspecs:
+ self._ifspecs.add(ifspec)
+ self._ifsUp.add(interface)
+ self._ifsDown.discard(interface)
+
+ def remove(self, interface, connection = None, protocol = None):
+ """
+ Removes a matching entry from the DB.
+
+ If no matching entry can be fonud, the operation returns successfully.
+ """
+ ifspec = f"{interface}:{connection}:{protocol}" if (connection is not None and protocol is not None) else interface
+ if ifspec in self._ifspecs:
+ self._ifspecs.remove(ifspec)
+ interface_remains = False
+ for ifspec in self._ifspecs:
+ if ifspec.split(':')[0] == interface:
+ interface_remains = True
+
+ if not interface_remains:
+ self._ifsDown.add(interface)
+ self._ifsUp.discard(interface)
+
+ def wantsInterfaceUp(self, interface):
+ """ Returns whether the DB contains at least one entry referencing the given interface """
+ for ifspec in self._ifspecs:
+ if ifspec.split(':')[0] == interface:
+ return True
+
+ return False
+
+ def removeAllOtherInterfaces(self, interface_list):
+ """ Removes all interfaces not included in the given list from the DB """
+ updated_ifspecs = set([ifspec for ifspec in self._ifspecs if ifspec.split(':')[0] in interface_list])
+ removed_ifspecs = self._ifspecs - updated_ifspecs
+ self._ifspecs = updated_ifspecs
+ interfaces_to_bring_down = [ifspec.split(':')[0] for ifspec in removed_ifspecs]
+ self._ifsDown.update(interfaces_to_bring_down)
+ self._ifsUp.difference_update(interfaces_to_bring_down)
+
+ def setPersistentInterfaces(self, interface_list):
+ """ Updates the set of persistently up interfaces to match the given list """
+ new_presistent_interfaces = set(interface_list)
+ current_presistent_interfaces = set([ifspec for ifspec in self._ifspecs if ':' not in ifspec])
+ added_presistent_interfaces = new_presistent_interfaces - current_presistent_interfaces
+ removed_presistent_interfaces = current_presistent_interfaces - new_presistent_interfaces
+
+ for interface in added_presistent_interfaces:
+ self.add(interface)
+
+ for interface in removed_presistent_interfaces:
+ self.remove(interface)
+
+ def commit(self, interface_dict_supplier):
+ """
+ Writes the DB to disk and brings interfaces up and down as needed.
+
+ Only interfaces referenced by entries modified in this DB session
+ are manipulated. If an interface is called to be brought up, the
+ provided interface_config_supplier function is invoked and expected
+ to return the config dictionary for the interface.
+ """
+ from vyos.ifconfig import VTIIf
+ from vyos.utils.process import call
+ from vyos.utils.network import get_interface_config
+
+ self._fileHandle.seek(0)
+ self._fileHandle.write(' '.join(self._ifspecs))
+ self._fileHandle.truncate()
+
+ for interface in self._ifsDown:
+ vti_link = get_interface_config(interface)
+ vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ if vti_link_up:
+ call(f'sudo ip link set {interface} down')
+ syslog(f'Interface {interface} is admin down ...')
+
+ self._ifsDown.clear()
+
+ for interface in self._ifsUp:
+ vti_link = get_interface_config(interface)
+ vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False)
+ if not vti_link_up:
+ vti = interface_dict_supplier(interface)
+ if 'disable' not in vti:
+ tmp = VTIIf(interface, bypass_vti_updown_db = True)
+ tmp.update(vti)
+ syslog(f'Interface {interface} is admin up ...')
+
+ self._ifsUp.clear()