diff options
38 files changed, 798 insertions, 284 deletions
diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 81f63c53b..34dfa529a 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -38,6 +38,9 @@ level={{ log.level }} [ipoe] verbose=1 +{% if lua_file is vyos_defined %} +lua-file={{ lua_file }} +{% endif %} {% if interface is vyos_defined %} {% for iface, iface_config in interface.items() %} {% set tmp = 'interface=' %} @@ -55,7 +58,10 @@ verbose=1 {% set range = 'range=' ~ iface_config.client_subnet ~ ',' if iface_config.client_subnet is vyos_defined else '' %} {% set relay = ',' ~ 'relay=' ~ iface_config.external_dhcp.dhcp_relay if iface_config.external_dhcp.dhcp_relay is vyos_defined else '' %} {% set giaddr = ',' ~ 'giaddr=' ~ iface_config.external_dhcp.giaddr if iface_config.external_dhcp.giaddr is vyos_defined else '' %} -{{ tmp }},{{ shared }}mode={{ iface_config.mode | upper }},ifcfg=1,{{ range }}start=dhcpv4,ipv6=1{{ relay }}{{ giaddr }} +{% set username = ',' ~ 'username=lua:' ~ iface_config.lua_username if iface_config.lua_username is vyos_defined else '' %} +{% set start_map = {'dhcp': 'dhcpv4', 'unclassified-packet': 'up', 'auto': 'auto'} %} +{% set start = start_map[iface_config.start_session] %} +{{ tmp }},{{ shared }}mode={{ iface_config.mode | upper }},ifcfg=1,{{ range }}start={{ start }},ipv6=1{{ relay }}{{ giaddr }}{{ username }} {% if iface_config.vlan_mon is vyos_defined %} vlan-mon={{ iface }},{{ iface_config.vlan | join(',') }} {% endif %} diff --git a/data/templates/getty/serial-getty.service.j2 b/data/templates/getty/serial-getty.service.j2 index 0183eae7d..687b05b6d 100644 --- a/data/templates/getty/serial-getty.service.j2 +++ b/data/templates/getty/serial-getty.service.j2 @@ -22,7 +22,7 @@ Before=rescue.service # The '-o' option value tells agetty to replace 'login' arguments with an # option to preserve environment (-p), followed by '--' for safety, and then # the entered username. -ExecStart=-/sbin/agetty -o '-p -- \\u' --keep-baud {{ speed }} %I $TERM +ExecStart=-/sbin/agetty -o '-p -- \\u' %I {{ speed }} $TERM Type=idle Restart=always UtmpIdentifier=%I diff --git a/data/templates/ipsec/swanctl.conf.j2 b/data/templates/ipsec/swanctl.conf.j2 index 698a9135e..64e7ea860 100644 --- a/data/templates/ipsec/swanctl.conf.j2 +++ b/data/templates/ipsec/swanctl.conf.j2 @@ -87,7 +87,11 @@ secrets { id-{{ gen_uuid }} = "{{ id }}" {% endfor %} {% endif %} +{% if psk_config.secret_type is vyos_defined('base64') %} + secret = 0s{{ psk_config.secret }} +{% elif psk_config.secret_type is vyos_defined('plaintext') %} secret = "{{ psk_config.secret }}" +{% endif %} } {% endfor %} {% endif %} diff --git a/data/templates/mdns-repeater/avahi-daemon.conf.j2 b/data/templates/mdns-repeater/avahi-daemon.conf.j2 index cc6495817..a5031945c 100644 --- a/data/templates/mdns-repeater/avahi-daemon.conf.j2 +++ b/data/templates/mdns-repeater/avahi-daemon.conf.j2 @@ -6,6 +6,9 @@ allow-interfaces={{ interface | join(', ') }} {% if browse_domain is vyos_defined and browse_domain | length %} browse-domains={{ browse_domain | join(', ') }} {% endif %} +{% if cache_entries is vyos_defined %} +cache-entries-max={{ cache_entries }} +{% endif %} disallow-other-stacks=no [wide-area] diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 3dd1b3249..bd2ff820d 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -501,6 +501,7 @@ </properties> <children> #include <include/generic-description.xml.i> + #include <include/interface/mtu-68-16000.xml.i> <leafNode name="prefix"> <properties> <help>Prefix which allocated to that network</help> diff --git a/interface-definitions/include/qos/hfsc-m1.xml.i b/interface-definitions/include/qos/hfsc-m1.xml.i index 21b9c4f32..ca37f6ecf 100644 --- a/interface-definitions/include/qos/hfsc-m1.xml.i +++ b/interface-definitions/include/qos/hfsc-m1.xml.i @@ -27,6 +27,5 @@ <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description> </valueHelp> </properties> - <defaultValue>0bit</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/hfsc-m2.xml.i b/interface-definitions/include/qos/hfsc-m2.xml.i index 24e8f5d63..816546657 100644 --- a/interface-definitions/include/qos/hfsc-m2.xml.i +++ b/interface-definitions/include/qos/hfsc-m2.xml.i @@ -27,6 +27,5 @@ <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description> </valueHelp> </properties> - <defaultValue>100%</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/version/qos-version.xml.i b/interface-definitions/include/version/qos-version.xml.i index c67e61e91..127f771a9 100644 --- a/interface-definitions/include/version/qos-version.xml.i +++ b/interface-definitions/include/version/qos-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/qos-version.xml.i --> -<syntaxVersion component='qos' version='2'></syntaxVersion> +<syntaxVersion component='qos' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index b922771c1..c69886a4b 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -35,6 +35,12 @@ <multi/> </properties> </leafNode> + <leafNode name="system-install"> + <properties> + <help>Install into CA certificate store on router</help> + <valueless/> + </properties> + </leafNode> #include <include/pki/cli-revoke.xml.i> </children> </tagNode> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index 907fd5e4c..c6ecb742e 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -85,78 +85,67 @@ <children> #include <include/generic-description.xml.i> #include <include/qos/bandwidth.xml.i> - <node name="flow-isolation"> + <leafNode name="flow-isolation"> <properties> <help>Flow isolation settings</help> + <completionHelp> + <list>blind src-host dst-host host flow dual-src-host dual-dst-host triple-isolate</list> + </completionHelp> + <valueHelp> + <format>blind</format> + <description>Disables flow isolation, all traffic passes through a single queue</description> + </valueHelp> + <valueHelp> + <format>src-host</format> + <description>Flows are defined only by source address</description> + </valueHelp> + <valueHelp> + <format>dst-host</format> + <description>Flows are defined only by destination address</description> + </valueHelp> + <valueHelp> + <format>host</format> + <description>Flows are defined by source-destination host pairs</description> + </valueHelp> + <valueHelp> + <format>flow</format> + <description>Flows are defined by the entire 5-tuple</description> + </valueHelp> + <valueHelp> + <format>dual-src-host</format> + <description>Flows are defined by the 5-tuple, fairness is applied first over source addresses, then over individual flows</description> + </valueHelp> + <valueHelp> + <format>dual-dst-host</format> + <description>Flows are defined by the 5-tuple, fairness is applied first over destination addresses, then over individual flows</description> + </valueHelp> + <valueHelp> + <format>triple-isolate</format> + <description>Flows are defined by the 5-tuple, fairness is applied over source and destination addresses and also over individual flows (default)</description> + </valueHelp> + <constraint> + <regex>(blind|src-host|dst-host|host|flow|dual-src-host|dual-dst-host|triple-isolate)</regex> + </constraint> </properties> - <children> - <leafNode name="blind"> - <properties> - <help>Disables flow isolation, all traffic passes through a single queue</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="src-host"> - <properties> - <help>Flows are defined only by source address</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="dst-host"> - <properties> - <help>Flows are defined only by destination address</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="host"> - <properties> - <help>Flows are defined by source-destination host pairs</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="flow"> - <properties> - <help>Flows are defined by the entire 5-tuple</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="dual-src-host"> - <properties> - <help>Flows are defined by the 5-tuple, fairness is applied first over source addresses, then over individual flows</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="dual-dst-host"> - <properties> - <help>Flows are defined by the 5-tuple, fairness is applied first over destination addresses, then over individual flows</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="triple-isolate"> - <properties> - <help>Flows are defined by the 5-tuple, fairness is applied over source and destination addresses and also over individual flows (default)</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="nat"> - <properties> - <help>Perform NAT lookup before applying flow-isolation rules</help> - <valueless/> - </properties> - </leafNode> - </children> - </node> + <defaultValue>triple-isolate</defaultValue> + </leafNode> + <leafNode name="flow-isolation-nat"> + <properties> + <help>Perform NAT lookup before applying flow-isolation rules</help> + <valueless/> + </properties> + </leafNode> <leafNode name="rtt"> <properties> <help>Round-Trip-Time for Active Queue Management (AQM)</help> <valueHelp> - <format>u32:1-3600000</format> + <format>u32:1-1000000000</format> <description>RTT in ms</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-3600000"/> + <validator name="numeric" argument="--range 1-1000000000"/> </constraint> - <constraintErrorMessage>RTT must be in range 1 to 3600000 milli-seconds</constraintErrorMessage> + <constraintErrorMessage>RTT must be in range 1 to 1000000000 milli-seconds</constraintErrorMessage> </properties> <defaultValue>100</defaultValue> </leafNode> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index 25bc43cc6..39cfb7889 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -131,6 +131,30 @@ </properties> <defaultValue>shared</defaultValue> </leafNode> + <leafNode name="start-session"> + <properties> + <help>Start session options</help> + <completionHelp> + <list>auto dhcp unclassified-packet</list> + </completionHelp> + <valueHelp> + <format>auto</format> + <description>Start session with username as the interface name</description> + </valueHelp> + <valueHelp> + <format>dhcp</format> + <description>Start session on DHCPv4 Discover</description> + </valueHelp> + <valueHelp> + <format>unclassified-packet</format> + <description>Start session on unclassified-packet</description> + </valueHelp> + <constraint> + <regex>(auto|dhcp|unclassified-packet)</regex> + </constraint> + </properties> + <defaultValue>dhcp</defaultValue> + </leafNode> <leafNode name="client-subnet"> <properties> <help>Client address pool</help> @@ -174,10 +198,34 @@ </leafNode> </children> </node> + <leafNode name="lua-username"> + <properties> + <help>Username function</help> + <valueHelp> + <format>txt</format> + <description>Name of the function in the Lua file to construct usernames with</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i> + </constraint> + </properties> + </leafNode> #include <include/accel-ppp/vlan.xml.i> #include <include/accel-ppp/vlan-mon.xml.i> </children> </tagNode> + <leafNode name="lua-file"> + <properties> + <help>Lua script file for constructing user names</help> + <valueHelp> + <format>filename</format> + <description>File with Lua script in /config/scripts directory</description> + </valueHelp> + <constraint> + <validator name="file-path" argument="--strict --parent-dir /config/scripts"/> + </constraint> + </properties> + </leafNode> #include <include/accel-ppp/client-ip-pool.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> #include <include/accel-ppp/default-pool.xml.i> diff --git a/interface-definitions/service_mdns_repeater.xml.in b/interface-definitions/service_mdns_repeater.xml.in index 5d6f61d74..9d626bf6a 100644 --- a/interface-definitions/service_mdns_repeater.xml.in +++ b/interface-definitions/service_mdns_repeater.xml.in @@ -67,6 +67,23 @@ <multi/> </properties> </leafNode> + <leafNode name="cache-entries"> + <properties> + <help>Number of resource records cached per interface</help> + <valueHelp> + <format>u32:0</format> + <description>Disable caching</description> + </valueHelp> + <valueHelp> + <format>u32:1-65535</format> + <description>Resource records to cache per interface</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>4096</defaultValue> + </leafNode> <leafNode name="vrrp-disable"> <properties> <help>Disables mDNS repeater on VRRP interfaces not in MASTER state</help> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index d9d6fd93b..5540021e2 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -41,6 +41,18 @@ </valueHelp> </properties> </leafNode> + <leafNode name="secret-type"> + <properties> + <help>Secret type</help> + <completionHelp> + <list>base64 plaintext</list> + </completionHelp> + <constraint> + <regex>(base64|plaintext)</regex> + </constraint> + </properties> + <defaultValue>plaintext</defaultValue> + </leafNode> </children> </tagNode> </children> diff --git a/op-mode-definitions/show-configuration.xml.in b/op-mode-definitions/show-configuration.xml.in index 5a2fdedfa..7ec718890 100644 --- a/op-mode-definitions/show-configuration.xml.in +++ b/op-mode-definitions/show-configuration.xml.in @@ -23,13 +23,6 @@ <!-- no admin check --> <command>cli-shell-api showCfg --show-active-only | vyos-config-to-commands</command> </node> - <node name="files"> - <properties> - <help> Show available saved configurations </help> - </properties> - <!-- no admin check --> - <command>${vyos_op_scripts_dir}/show_configuration_files.sh</command> - </node> <node name="json"> <properties> <help>Show running configuration in JSON format</help> diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index dec619d3e..425990967 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -36,7 +36,8 @@ directories = { 'isc_dhclient_dir' : '/run/dhclient', 'dhcp6_client_dir' : '/run/dhcp6c', 'vyos_configdir' : '/opt/vyatta/config', - 'completion_dir' : f'{base_dir}/completion' + 'completion_dir' : f'{base_dir}/completion', + 'ca_certificates' : '/usr/local/share/ca-certificates/vyos' } config_status = '/tmp/vyos-config-status' diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 002d3da9e..cd562e1fe 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -98,6 +98,10 @@ class Interface(Control): 'shellcmd': 'ip -json -detail link list dev {ifname}', 'format': lambda j: jmespath.search('[*].ifalias | [0]', json.loads(j)) or '', }, + 'ifindex': { + 'shellcmd': 'ip -json -detail link list dev {ifname}', + 'format': lambda j: jmespath.search('[*].ifindex | [0]', json.loads(j)) or '', + }, 'mac': { 'shellcmd': 'ip -json -detail link list dev {ifname}', 'format': lambda j: jmespath.search('[*].address | [0]', json.loads(j)), @@ -428,6 +432,17 @@ class Interface(Control): nft_command = f'add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}' self._nft_check_and_run(nft_command) + def get_ifindex(self): + """ + Get interface index by name + + Example: + >>> from vyos.ifconfig import Interface + >>> Interface('eth0').get_ifindex() + '2' + """ + return int(self.get_interface('ifindex')) + def get_min_mtu(self): """ Get hardware minimum supported MTU diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index 9030b1302..cccac361d 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -195,6 +195,9 @@ class WireGuardIf(Interface): base_cmd += f' private-key {tmp_file.name}' base_cmd = base_cmd.format(**config) + # T6490: execute command to ensure interface configured + self._cmd(base_cmd) + if 'peer' in config: for peer, peer_config in config['peer'].items(): # T4702: No need to configure this peer when it was explicitly diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 322cdca44..12d940e3c 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -17,6 +17,7 @@ import os import jmespath from vyos.base import Warning +from vyos.ifconfig import Interface from vyos.utils.process import cmd from vyos.utils.dict import dict_search from vyos.utils.file import read_file @@ -253,19 +254,24 @@ class QoSBase: for index, (match, match_config) in enumerate(cls_config['match'].items(), start=1): filter_cmd = filter_cmd_base if not has_filter: - for key in ['mark', 'vif', 'ip', 'ipv6']: + for key in ['mark', 'vif', 'ip', 'ipv6', 'interface']: if key in match_config: has_filter = True break - if self.qostype == 'shaper' and 'prio ' not in filter_cmd: + if self.qostype in ['shaper', 'shaper_hfsc'] and 'prio ' not in filter_cmd: filter_cmd += f' prio {index}' if 'mark' in match_config: mark = match_config['mark'] filter_cmd += f' handle {mark} fw' + if 'vif' in match_config: vif = match_config['vif'] filter_cmd += f' basic match "meta(vlan mask 0xfff eq {vif})"' + elif 'interface' in match_config: + iif_name = match_config['interface'] + iif = Interface(iif_name).get_ifindex() + filter_cmd += f' basic match "meta(rt_iif eq {iif})"' for af in ['ip', 'ipv6']: tc_af = af diff --git a/python/vyos/qos/cake.py b/python/vyos/qos/cake.py index 1ee7d0fc3..ca5a26917 100644 --- a/python/vyos/qos/cake.py +++ b/python/vyos/qos/cake.py @@ -15,10 +15,25 @@ from vyos.qos.base import QoSBase + class CAKE(QoSBase): + """ + https://man7.org/linux/man-pages/man8/tc-cake.8.html + """ + _direction = ['egress'] - # https://man7.org/linux/man-pages/man8/tc-cake.8.html + flow_isolation_map = { + 'blind': 'flowblind', + 'src-host': 'srchost', + 'dst-host': 'dsthost', + 'dual-dst-host': 'dual-dsthost', + 'dual-src-host': 'dual-srchost', + 'triple-isolate': 'triple-isolate', + 'flow': 'flows', + 'host': 'hosts', + } + def update(self, config, direction): tmp = f'tc qdisc add dev {self._interface} root handle 1: cake {direction}' if 'bandwidth' in config: @@ -30,26 +45,16 @@ class CAKE(QoSBase): tmp += f' rtt {rtt}ms' if 'flow_isolation' in config: - if 'blind' in config['flow_isolation']: - tmp += f' flowblind' - if 'dst_host' in config['flow_isolation']: - tmp += f' dsthost' - if 'dual_dst_host' in config['flow_isolation']: - tmp += f' dual-dsthost' - if 'dual_src_host' in config['flow_isolation']: - tmp += f' dual-srchost' - if 'triple_isolate' in config['flow_isolation']: - tmp += f' triple-isolate' - if 'flow' in config['flow_isolation']: - tmp += f' flows' - if 'host' in config['flow_isolation']: - tmp += f' hosts' - if 'nat' in config['flow_isolation']: - tmp += f' nat' - if 'src_host' in config['flow_isolation']: - tmp += f' srchost ' - else: - tmp += f' nonat' + isolation_value = self.flow_isolation_map.get(config['flow_isolation']) + + if isolation_value is not None: + tmp += f' {isolation_value}' + else: + raise ValueError( + f'Invalid flow isolation parameter: {config["flow_isolation"]}' + ) + + tmp += ' nat' if 'flow_isolation_nat' in config else ' nonat' self._cmd(tmp) diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py index 8b0333c21..9f92ccd8b 100644 --- a/python/vyos/qos/trafficshaper.py +++ b/python/vyos/qos/trafficshaper.py @@ -126,91 +126,71 @@ class TrafficShaper(QoSBase): # call base class super().update(config, direction) + class TrafficShaperHFSC(QoSBase): + """ + Traffic shaper using Hierarchical Fair Service Curve (HFSC). + Documentation: https://man7.org/linux/man-pages/man8/tc-hfsc.8.html + """ + _parent = 1 qostype = 'shaper_hfsc' - # https://man7.org/linux/man-pages/man8/tc-hfsc.8.html - def update(self, config, direction): - class_id_max = 0 - if 'class' in config: - tmp = list(config['class']) - tmp.sort() - class_id_max = tmp[-1] + criteria = ['linkshare', 'realtime', 'upperlimit'] + short_criterion = { + 'linkshare': 'ls', + 'realtime': 'rt', + 'upperlimit': 'ul', + } + + def _gen_class(self, cls: int, cls_config: dict): + """ + Generate HFSC class and add Stochastic Fair Queueing (SFQ) qdisc. + + Args: + cls (int): Class ID + cls_config (dict): Configuration for the class + """ + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} hfsc' + + for crit in self.criteria: + param = cls_config.get(crit) + if param: + tmp += ( + f' {self.short_criterion[crit]}' + f' m1 {self._rate_convert(param["m1"]) if param.get("m1") else 0}' + f' d {param.get("d", 0)}ms' + f' m2 {self._rate_convert(param["m2"])}' + ) - r2q = 10 - # bandwidth is a mandatory CLI node - speed = self._rate_convert(config['bandwidth']) - speed_bps = int(speed) // 8 + self._cmd(tmp) - # need a bigger r2q if going fast than 16 mbits/sec - if (speed_bps // r2q) >= MAXQUANTUM: # integer division - r2q = ceil(speed_bps // MAXQUANTUM) - else: - # if there is a slow class then may need smaller value - if 'class' in config: - min_speed = speed_bps - for cls, cls_options in config['class'].items(): - # find class with the lowest bandwidth used - if 'bandwidth' in cls_options: - bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second - if bw_bps < min_speed: - min_speed = bw_bps + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq perturb 10' + self._cmd(tmp) - while (r2q > 1) and (min_speed // r2q) < MINQUANTUM: - tmp = r2q -1 - if (speed_bps // tmp) >= MAXQUANTUM: - break - r2q = tmp + def update(self, config, direction): + class_id_max = self._get_class_max_id(config) + default_cls_id = int(class_id_max) + 1 if class_id_max else 2 - default_minor_id = int(class_id_max) +1 - tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: hfsc default {default_minor_id:x}' # default is in hex + speed = self._rate_convert(config['bandwidth']) + + tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: hfsc default {default_cls_id:x}' # default is in hex self._cmd(tmp) tmp = f'tc class replace dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 hfsc sc rate {speed} ul rate {speed}' self._cmd(tmp) + # tmp = f'tc qdisc add dev {self._interface} parent {self._parent:x}:1 handle f1: sfq perturb 10' + # self._cmd(tmp) + if 'class' in config: for cls, cls_config in config['class'].items(): - # class id is used later on and passed as hex, thus this needs to be an int - cls = int(cls) - # ls m1 - if cls_config.get('linkshare', {}).get('m1').endswith('%'): - percent = cls_config['linkshare']['m1'].rstrip('%') - m_one_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 - else: - m_one_rate = cls_config['linkshare']['m1'] - # ls m2 - if cls_config.get('linkshare', {}).get('m2').endswith('%'): - percent = cls_config['linkshare']['m2'].rstrip('%') - m_two_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 - else: - m_two_rate = self._rate_convert(cls_config['linkshare']['m2']) - - tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} ' - self._cmd(tmp) - - tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq perturb 10' - self._cmd(tmp) + self._gen_class(cls=int(cls), cls_config=cls_config) if 'default' in config: - # ls m1 - if config.get('default', {}).get('linkshare', {}).get('m1').endswith('%'): - percent = config['default']['linkshare']['m1'].rstrip('%') - m_one_rate = self._rate_convert(config['default']['linkshare']['m1']) * int(percent) // 100 - else: - m_one_rate = config['default']['linkshare']['m1'] - # ls m2 - if config.get('default', {}).get('linkshare', {}).get('m2').endswith('%'): - percent = config['default']['linkshare']['m2'].rstrip('%') - m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) * int(percent) // 100 - else: - m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) - tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} ' - self._cmd(tmp) - - tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq perturb 10' - self._cmd(tmp) + self._gen_class( + cls=int(default_cls_id), cls_config=config.get('default', {}) + ) # call base class super().update(config, direction) diff --git a/python/vyos/utils/convert.py b/python/vyos/utils/convert.py index dd4266f57..2f587405d 100644 --- a/python/vyos/utils/convert.py +++ b/python/vyos/utils/convert.py @@ -235,3 +235,29 @@ def convert_data(data) -> dict | list | tuple | str | int | float | bool | None: # which cannot be converted to JSON # for example: complex | range | memoryview return + + +def encode_to_base64(input_string): + """ + Encodes a given string to its base64 representation. + + Args: + input_string (str): The string to be encoded. + + Returns: + str: The base64-encoded version of the input string. + + Example: + input_string = "Hello, World!" + encoded_string = encode_to_base64(input_string) + print(encoded_string) # Output: SGVsbG8sIFdvcmxkIQ== + """ + import base64 + # Convert the string to bytes + byte_string = input_string.encode('utf-8') + + # Encode the byte string to base64 + encoded_string = base64.b64encode(byte_string) + + # Decode the base64 bytes back to a string + return encoded_string.decode('utf-8') diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py index c03b9eb44..0541384da 100755 --- a/smoketest/scripts/cli/test_container.py +++ b/smoketest/scripts/cli/test_container.py @@ -224,6 +224,22 @@ class TestContainer(VyOSUnitTestSHIM.TestCase): n = cmd_to_json(f'sudo podman network inspect {net_name}') self.assertEqual(n['dns_enabled'], False) + def test_network_mtu(self): + prefix = '192.0.2.0/24' + base_name = 'ipv4' + net_name = 'NET01' + + self.cli_set(base_path + ['network', net_name, 'prefix', prefix]) + self.cli_set(base_path + ['network', net_name, 'mtu', '1280']) + + name = f'{base_name}-2' + self.cli_set(base_path + ['name', name, 'image', cont_image]) + self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)]) + self.cli_commit() + + n = cmd_to_json(f'sudo podman network inspect {net_name}') + self.assertEqual(n['options']['mtu'], '1280') + def test_uid_gid(self): cont_name = 'uid-test' gid = '100' diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py index aaeebcdae..79b791288 100755 --- a/smoketest/scripts/cli/test_qos.py +++ b/smoketest/scripts/cli/test_qos.py @@ -21,7 +21,8 @@ from json import loads from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError -from vyos.ifconfig import Section +from vyos.ifconfig import Section, Interface +from vyos.qos import CAKE from vyos.utils.process import cmd base_path = ['qos'] @@ -871,6 +872,68 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): self.cli_set(['qos', 'traffic-match-group', '3', 'match-group', 'unexpected']) self.cli_commit() + def test_17_cake_updates(self): + bandwidth = 1000000 + rtt = 200 + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set( + base_path + ['policy', 'cake', policy_name, 'bandwidth', str(bandwidth)] + ) + self.cli_set(base_path + ['policy', 'cake', policy_name, 'rtt', str(rtt)]) + + # commit changes + self.cli_commit() + + tmp = get_tc_qdisc_json(interface) + + self.assertEqual('cake', tmp['kind']) + # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) + self.assertEqual(int(bandwidth * 125), tmp['options']['bandwidth']) + # RTT internally is in us + self.assertEqual(int(rtt * 1000), tmp['options']['rtt']) + self.assertEqual('triple-isolate', tmp['options']['flowmode']) + self.assertFalse(tmp['options']['ingress']) + self.assertFalse(tmp['options']['nat']) + self.assertTrue(tmp['options']['raw']) + + nat = True + for flow_isolation in [ + 'blind', + 'src-host', + 'dst-host', + 'dual-dst-host', + 'dual-src-host', + 'triple-isolate', + 'flow', + 'host', + ]: + self.cli_set( + base_path + + ['policy', 'cake', policy_name, 'flow-isolation', flow_isolation] + ) + + if nat: + self.cli_set( + base_path + ['policy', 'cake', policy_name, 'flow-isolation-nat'] + ) + else: + self.cli_delete( + base_path + ['policy', 'cake', policy_name, 'flow-isolation-nat'] + ) + + self.cli_commit() + + tmp = get_tc_qdisc_json(interface) + self.assertEqual( + CAKE.flow_isolation_map.get(flow_isolation), tmp['options']['flowmode'] + ) + + self.assertEqual(nat, tmp['options']['nat']) + nat = not nat + def test_20_round_robin_policy_default(self): interface = self._interfaces[0] policy_name = f'qos-policy-{interface}' @@ -922,6 +985,137 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): tmp[2]['options'], ) + def test_21_shaper_hfsc(self): + interface = self._interfaces[0] + policy_name = f'qos-policy-{interface}' + ul = { + 'm1': '100kbit', + 'm2': '150kbit', + 'd': '100', + } + ls = {'m2': '120kbit'} + rt = { + 'm1': '110kbit', + 'm2': '130kbit', + 'd': '75', + } + self.cli_set(base_path + ['interface', interface, 'egress', policy_name]) + self.cli_set(base_path + ['policy', 'shaper-hfsc', policy_name]) + + # Policy {policy_name} misses "default" class! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit'] + ) + + # At least one m2 value needs to be set for class: {class_name} + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'm1', ul['m1']] + ) + # {class_name} upperlimit m1 value is set, but no m2 was found! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'm2', ul['m2']] + ) + # {class_name} upperlimit m1 value is set, but no d was found! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'upperlimit', 'd', ul['d']] + ) + # Linkshare m2 needs to be defined to use upperlimit m2 for class: {class_name} + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'linkshare', 'm2', ls['m2']] + ) + self.cli_commit() + + # use raw because tc json is incorrect here + tmp = cmd(f'tc -details qdisc show dev {interface}') + for rec in tmp.split('\n'): + rec = rec.strip() + if 'root' in rec: + self.assertEqual(rec, 'qdisc hfsc 1: root refcnt 2 default 2') + else: + self.assertRegex( + rec, + r'qdisc sfq \S+: parent 1:2 limit 127p quantum 1514b depth 127 flows 128 divisor 1024 perturb 10sec', + ) + # use raw because tc json is incorrect here + tmp = cmd(f'tc -details class show dev {interface}') + for rec in tmp.split('\n'): + rec = rec.strip().lower() + if 'root' in rec: + self.assertEqual(rec, 'class hfsc 1: root') + elif 'hfsc 1:1' in rec: + # m2 \S+bit is auto bandwidth + self.assertRegex( + rec, + r'class hfsc 1:1 parent 1: sc m1 0bit d 0us m2 \S+bit ul m1 0bit d 0us m2 \S+bit', + ) + else: + self.assertRegex( + rec, + rf'class hfsc 1:2 parent 1:1 leaf \S+: ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}', + ) + + for key, val in rt.items(): + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'default', 'realtime', key, val] + ) + self.cli_commit() + + tmp = cmd(f'tc -details class show dev {interface}') + for rec in tmp.split('\n'): + rec = rec.strip().lower() + if 'hfsc 1:2' in rec: + self.assertTrue( + f'rt m1 {rt["m1"]} d {rt["d"]}ms m2 {rt["m2"]} ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}' + in rec + ) + + # add some class + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '10', 'linkshare', 'm2', '300kbit'] + ) + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '10', 'match', 'tst', 'ip', 'dscp', 'internet'] + ) + + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'realtime', 'm2', '250kbit'] + ) + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'realtime', 'd', '77'] + ) + self.cli_set( + base_path + ['policy', 'shaper-hfsc', policy_name, 'class', '30', 'match', 'tst30', 'ip', 'dscp', 'critical'] + ) + self.cli_commit() + + tmp = cmd(f'tc -details qdisc show dev {interface}') + self.assertEqual(4, len(tmp.split('\n'))) + + tmp = cmd(f'tc -details class show dev {interface}') + tmp = tmp.lower() + + self.assertTrue( + f'rt m1 {rt["m1"]} d {rt["d"]}ms m2 {rt["m2"]} ls m1 0bit d 0us m2 {ls["m2"]} ul m1 {ul["m1"]} d {ul["d"]}ms m2 {ul["m2"]}' + in tmp + ) + self.assertTrue(': ls m1 0bit d 0us m2 300kbit' in tmp) + self.assertTrue(': rt m1 0bit d 77ms m2 250kbit' in tmp) + def test_22_rate_control_default(self): interface = self._interfaces[0] policy_name = f'qos-policy-{interface}' @@ -943,6 +1137,30 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): # TC store rates as a 32-bit unsigned integer in bps (Bytes per second) self.assertEqual(int(bandwidth * 125), tmp['options']['rate']) + def test_23_policy_limiter_iif_filter(self): + policy_name = 'smoke_test' + base_policy_path = ['qos', 'policy', 'limiter', policy_name] + + self.cli_set(['qos', 'interface', self._interfaces[0], 'ingress', policy_name]) + self.cli_set(base_policy_path + ['class', '100', 'bandwidth', '20gbit']) + self.cli_set(base_policy_path + ['class', '100', 'burst', '3760k']) + self.cli_set(base_policy_path + ['class', '100', 'match', 'test', 'interface', self._interfaces[0]]) + self.cli_set(base_policy_path + ['class', '100', 'priority', '20']) + self.cli_set(base_policy_path + ['default', 'bandwidth', '1gbit']) + self.cli_set(base_policy_path + ['default', 'burst', '125000000b']) + self.cli_commit() + + iif = Interface(self._interfaces[0]).get_ifindex() + tc_filters = cmd(f'tc filter show dev {self._interfaces[0]} ingress') + + # class 100 + self.assertIn('filter parent ffff: protocol all pref 20 basic chain 0', tc_filters) + self.assertIn(f'meta(rt_iif eq {iif})', tc_filters) + self.assertIn('action order 1: police 0x1 rate 20Gbit burst 3847500b mtu 2Kb action drop overhead 0b', tc_filters) + # default + self.assertIn('filter parent ffff: protocol all pref 255 basic chain 0', tc_filters) + self.assertIn('action order 1: police 0x2 rate 1Gbit burst 125000000b mtu 2Kb action drop overhead 0b', tc_filters) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index 46c4e25a1..f891bf295 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -557,6 +557,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', router]) + self.cli_set(pool + ['range', '0', 'option', 'default-router', router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -569,6 +570,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST') self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + pool_obj = { + 'pool': f'{range_0_start} - {range_0_stop}', + 'option-data': [{'name': 'routers', 'data': router}] + } + # Verify options self.verify_config_object( obj, @@ -579,7 +585,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop}'}) + pool_obj) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) @@ -600,6 +606,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_set(pool + ['subnet-id', '1']) self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', exclude_addr]) + self.cli_set(pool + ['range', '0', 'option', 'default-router', router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -612,6 +619,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2') self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + pool_obj = { + 'pool': f'{range_0_start} - {range_0_stop_excl}', + 'option-data': [{'name': 'routers', 'data': router}] + } + + pool_exclude_obj = { + 'pool': f'{range_0_start_excl} - {range_0_stop}', + 'option-data': [{'name': 'routers', 'data': router}] + } + # Verify options self.verify_config_object( obj, @@ -621,12 +638,12 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start} - {range_0_stop_excl}'}) + pool_obj) self.verify_config_object( obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], - {'pool': f'{range_0_start_excl} - {range_0_stop}'}) + pool_exclude_obj) # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) diff --git a/smoketest/scripts/cli/test_service_mdns_repeater.py b/smoketest/scripts/cli/test_service_mdns_repeater.py index f2fb3b509..30e48683f 100755 --- a/smoketest/scripts/cli/test_service_mdns_repeater.py +++ b/smoketest/scripts/cli/test_service_mdns_repeater.py @@ -21,36 +21,45 @@ from base_vyostest_shim import VyOSUnitTestSHIM from configparser import ConfigParser from vyos.configsession import ConfigSessionError from vyos.utils.process import process_named_running +from vyos.xml_ref import default_value base_path = ['service', 'mdns', 'repeater'] intf_base = ['interfaces', 'dummy'] config_file = '/run/avahi-daemon/avahi-daemon.conf' - class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): - def setUp(self): - # Start with a clean CLI instance - self.cli_delete(base_path) + @classmethod + def setUpClass(cls): + super(TestServiceMDNSrepeater, cls).setUpClass() - # Service required a configured IP address on the interface - self.cli_set(intf_base + ['dum10', 'address', '192.0.2.1/30']) - self.cli_set(intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local']) - self.cli_set(intf_base + ['dum20', 'address', '192.0.2.5/30']) - self.cli_set(intf_base + ['dum20', 'address', '2001:db8:0:2::5/64']) - self.cli_set(intf_base + ['dum30', 'address', '192.0.2.9/30']) - self.cli_set(intf_base + ['dum30', 'address', '2001:db8:0:2::9/64']) - self.cli_set(intf_base + ['dum40', 'address', '2001:db8:0:2::11/64']) - self.cli_commit() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, intf_base + ['dum10', 'address', '192.0.2.1/30']) + cls.cli_set(cls, intf_base + ['dum10', 'ipv6', 'address', 'no-default-link-local']) + cls.cli_set(cls, intf_base + ['dum20', 'address', '192.0.2.5/30']) + cls.cli_set(cls, intf_base + ['dum20', 'address', '2001:db8:0:2::5/64']) + cls.cli_set(cls, intf_base + ['dum30', 'address', '192.0.2.9/30']) + cls.cli_set(cls, intf_base + ['dum30', 'address', '2001:db8:0:2::9/64']) + cls.cli_set(cls, intf_base + ['dum40', 'address', '2001:db8:0:2::11/64']) + + cls.cli_commit(cls) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, intf_base + ['dum10']) + cls.cli_delete(cls, intf_base + ['dum20']) + cls.cli_delete(cls, intf_base + ['dum30']) + cls.cli_delete(cls, intf_base + ['dum40']) + + cls.cli_commit(cls) def tearDown(self): # Check for running process self.assertTrue(process_named_running('avahi-daemon')) self.cli_delete(base_path) - self.cli_delete(intf_base + ['dum10']) - self.cli_delete(intf_base + ['dum20']) - self.cli_delete(intf_base + ['dum30']) - self.cli_delete(intf_base + ['dum40']) self.cli_commit() # Check that there is no longer a running process @@ -130,5 +139,38 @@ class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): self.assertEqual(conf['server']['allow-interfaces'], 'dum30, dum40') self.assertEqual(conf['reflector']['enable-reflector'], 'yes') + def test_service_max_cache_entries(self): + cli_default_max_cache = default_value(base_path + ['cache-entries']) + self.cli_set(base_path) + + # Need at least two interfaces + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['interface', 'dum20']) + + # Need at least two interfaces + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['interface', 'dum30']) + + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(delimiters='=') + conf.read(config_file) + self.assertEqual(conf['server']['cache-entries-max'], cli_default_max_cache) + + # Set max cache entries + cache_entries = '1234' + self.cli_set(base_path + ['cache-entries', cache_entries]) + + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(delimiters='=') + conf.read(config_file) + + self.assertEqual(conf['server']['cache-entries-max'], cache_entries) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index de18d0427..f2bea58d1 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -21,6 +21,7 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.ifconfig import Interface +from vyos.utils.convert import encode_to_base64 from vyos.utils.process import process_named_running from vyos.utils.file import read_file @@ -495,6 +496,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): local_id = 'vyos-r1' remote_id = 'vyos-r2' peer_base_path = base_path + ['site-to-site', 'peer', connection_name] + secret_base64 = encode_to_base64(secret) self.cli_set(tunnel_path + ['tun1', 'encapsulation', 'gre']) self.cli_set(tunnel_path + ['tun1', 'source-address', local_address]) @@ -509,7 +511,8 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_address]) self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip]) - self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret_base64]) + self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret-type', 'base64']) self.cli_set(peer_base_path + ['authentication', 'local-id', local_id]) self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret']) @@ -546,7 +549,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): f'id-{regex_uuid4} = "{remote_id}"', f'id-{regex_uuid4} = "{peer_ip}"', f'id-{regex_uuid4} = "{local_address}"', - f'secret = "{secret}"', + f'secret = 0s{secret_base64}', ] for line in swanctl_secrets_lines: diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 14387cbbf..a7dc33d9d 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -419,12 +419,18 @@ def generate(container): 'dns_enabled': True, 'ipam_options': { 'driver': 'host-local' + }, + 'options': { + 'mtu': '1500' } } if 'no_name_server' in network_config: tmp['dns_enabled'] = False + if 'mtu' in network_config: + tmp['options']['mtu'] = network_config['mtu'] + for prefix in network_config['prefix']: net = {'subnet': prefix, 'gateway': inc_ip(prefix, 1)} tmp['subnets'].append(net) diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index 7abdfdbfa..b6fd6b0b2 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -70,9 +70,6 @@ def verify(wireguard): if 'private_key' not in wireguard: raise ConfigError('Wireguard private-key not defined') - if 'peer' not in wireguard: - raise ConfigError('At least one Wireguard peer is required!') - if 'port' in wireguard and 'port_changed' in wireguard: listen_port = int(wireguard['port']) if check_port_availability('0.0.0.0', listen_port, 'udp') is not True: @@ -80,28 +77,29 @@ def verify(wireguard): 'cannot be used for the interface!') # run checks on individual configured WireGuard peer - public_keys = [] - for tmp in wireguard['peer']: - peer = wireguard['peer'][tmp] + if 'peer' in wireguard: + public_keys = [] + for tmp in wireguard['peer']: + peer = wireguard['peer'][tmp] - if 'allowed_ips' not in peer: - raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') + if 'allowed_ips' not in peer: + raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') - if 'public_key' not in peer: - raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') + if 'public_key' not in peer: + raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') - if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): - raise ConfigError('Both Wireguard port and address must be defined ' - f'for peer "{tmp}" if either one of them is set!') + if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): + raise ConfigError('Both Wireguard port and address must be defined ' + f'for peer "{tmp}" if either one of them is set!') - if peer['public_key'] in public_keys: - raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') + if peer['public_key'] in public_keys: + raise ConfigError(f'Duplicate public-key defined on peer "{tmp}"') - if 'disable' not in peer: - if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']): - raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"') + if 'disable' not in peer: + if is_wireguard_key_pair(wireguard['private_key'], peer['public_key']): + raise ConfigError(f'Peer "{tmp}" has the same public key as the interface "{wireguard["ifname"]}"') - public_keys.append(peer['public_key']) + public_keys.append(peer['public_key']) def generate(wireguard): return None diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 45e0129a3..acea2c9be 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -50,6 +50,7 @@ from vyos import airbag airbag.enable() vyos_certbot_dir = directories['certbot'] +vyos_ca_certificates_dir = directories['ca_certificates'] # keys to recursively search for under specified path sync_search = [ @@ -149,35 +150,15 @@ def get_config(config=None): if len(argv) > 1 and argv[1] == 'certbot_renew': pki['certbot_renew'] = {} - tmp = node_changed(conf, base + ['ca'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'ca' : tmp}) - - tmp = node_changed(conf, base + ['certificate'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'certificate' : tmp}) + changed_keys = ['ca', 'certificate', 'dh', 'key-pair', 'openssh', 'openvpn'] - tmp = node_changed(conf, base + ['dh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'dh' : tmp}) + for key in changed_keys: + tmp = node_changed(conf, base + [key], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - tmp = node_changed(conf, base + ['key-pair'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'key_pair' : tmp}) - - tmp = node_changed(conf, base + ['openssh'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'openssh' : tmp}) + if 'changed' not in pki: + pki.update({'changed':{}}) - tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD) - if tmp: - if 'changed' not in pki: pki.update({'changed':{}}) - pki['changed'].update({'openvpn' : tmp}) + pki['changed'].update({key.replace('-', '_') : tmp}) # We only merge on the defaults of there is a configuration at all if conf.exists(base): @@ -417,10 +398,33 @@ def verify(pki): return None +def cleanup_system_ca(): + if not os.path.exists(vyos_ca_certificates_dir): + os.mkdir(vyos_ca_certificates_dir) + else: + for filename in os.listdir(vyos_ca_certificates_dir): + full_path = os.path.join(vyos_ca_certificates_dir, filename) + if os.path.isfile(full_path): + os.unlink(full_path) + def generate(pki): if not pki: + cleanup_system_ca() return None + # Create or cleanup CA install directory + if 'changed' in pki and 'ca' in pki['changed']: + cleanup_system_ca() + + if 'ca' in pki: + for ca, ca_conf in pki['ca'].items(): + if 'system_install' in ca_conf: + ca_obj = load_certificate(ca_conf['certificate']) + ca_path = os.path.join(vyos_ca_certificates_dir, f'{ca}.crt') + + with open(ca_path, 'w') as f: + f.write(encode_certificate(ca_obj)) + # Certbot renewal only needs to re-trigger the services to load up the # new PEM file if 'certbot_renew' in pki: @@ -487,6 +491,7 @@ def apply(pki): systemd_certbot_name = 'certbot.timer' if not pki: call(f'systemctl stop {systemd_certbot_name}') + call('update-ca-certificates') return None has_certbot = False @@ -504,6 +509,10 @@ def apply(pki): if 'changed' in pki: call_dependents() + # Rebuild ca-certificates bundle + if 'ca' in pki['changed']: + call('update-ca-certificates') + return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py index d323ceb4f..c8894fd41 100755 --- a/src/conf_mode/protocols_static_multicast.py +++ b/src/conf_mode/protocols_static_multicast.py @@ -86,10 +86,10 @@ def verify(mroute): if mroute is None: return None - for route in mroute['mroute']: - route = route.split('/') + for mcast_route in mroute['mroute']: + route = mcast_route.split('/') if IPv4Address(route[0]) < IPv4Address('224.0.0.0'): - raise ConfigError(route + " not a multicast network") + raise ConfigError(f'{mcast_route} not a multicast network') def generate(mroute): diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index a4d5f44e7..59e307a39 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -198,10 +198,16 @@ def get_config(config=None): def _verify_match(cls_config: dict) -> None: if 'match' in cls_config: for match, match_config in cls_config['match'].items(): - if {'ip', 'ipv6'} <= set(match_config): + filters = set(match_config) + if {'ip', 'ipv6'} <= filters: raise ConfigError( f'Can not use both IPv6 and IPv4 in one match ({match})!') + if {'interface', 'vif'} & filters: + if {'ip', 'ipv6', 'ether'} & filters: + raise ConfigError( + f'Can not combine protocol and interface or vlan tag match ({match})!') + def _verify_match_group_exist(cls_config, qos): if 'match_group' in cls_config: @@ -210,6 +216,46 @@ def _verify_match_group_exist(cls_config, qos): Warning(f'Match group "{group}" does not exist!') +def _verify_default_policy_exist(policy, policy_config): + if 'default' not in policy_config: + raise ConfigError(f'Policy {policy} misses "default" class!') + + +def _check_shaper_hfsc_rate(cls, cls_conf): + is_m2_exist = False + for crit in TrafficShaperHFSC.criteria: + if cls_conf.get(crit, {}).get('m2') is not None: + is_m2_exist = True + + if cls_conf.get(crit, {}).get('m1') is not None: + for crit_val in ['m2', 'd']: + if cls_conf.get(crit, {}).get(crit_val) is None: + raise ConfigError( + f'{cls} {crit} m1 value is set, but no {crit_val} was found!' + ) + + if not is_m2_exist: + raise ConfigError(f'At least one m2 value needs to be set for class: {cls}') + + if ( + cls_conf.get('upperlimit', {}).get('m2') is not None + and cls_conf.get('linkshare', {}).get('m2') is None + ): + raise ConfigError( + f'Linkshare m2 needs to be defined to use upperlimit m2 for class: {cls}' + ) + + +def _verify_shaper_hfsc(policy, policy_config): + _verify_default_policy_exist(policy, policy_config) + + _check_shaper_hfsc_rate('default', policy_config.get('default')) + + if 'class' in policy_config: + for cls, cls_conf in policy_config['class'].items(): + _check_shaper_hfsc_rate(cls, cls_conf) + + def verify(qos): if not qos or 'interface' not in qos: return None @@ -253,11 +299,13 @@ def verify(qos): if queue_lim < max_tr: raise ConfigError(f'Policy "{policy}" uses queue-limit "{queue_lim}" < max-threshold "{max_tr}"!') if policy_type in ['priority_queue']: - if 'default' not in policy_config: - raise ConfigError(f'Policy {policy} misses "default" class!') + _verify_default_policy_exist(policy, policy_config) if policy_type in ['rate_control']: if 'bandwidth' not in policy_config: raise ConfigError('Bandwidth not defined') + if policy_type in ['shaper_hfsc']: + _verify_shaper_hfsc(policy, policy_config) + if 'default' in policy_config: if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin', 'shaper_hfsc']: raise ConfigError('Bandwidth not defined for default traffic!') @@ -293,6 +341,7 @@ def generate(qos): return None + def apply(qos): # Always delete "old" shapers first for interface in interfaces(): diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index e89448e2d..9c59aa63d 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -87,6 +87,10 @@ def dhcp_slice_range(exclude_list, range_dict): 'start' : range_start, 'stop' : str(ip_address(e) -1) } + + if 'option' in range_dict: + r['option'] = range_dict['option'] + # On the next run our address range will start one address after # the exclude address range_start = str(ip_address(e) + 1) @@ -104,6 +108,10 @@ def dhcp_slice_range(exclude_list, range_dict): 'start': str(ip_address(e) + 1), 'stop': str(range_stop) } + + if 'option' in range_dict: + r['option'] = range_dict['option'] + if not (ip_address(r['start']) > ip_address(r['stop'])): output.append(r) else: diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index c7e3ef033..a14d4b5b6 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -31,6 +31,7 @@ from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos.accel_ppp_util import verify_accel_ppp_authentication from vyos import ConfigError from vyos import airbag + airbag.enable() @@ -52,7 +53,9 @@ def get_config(config=None): if dict_search('client_ip_pool', ipoe): # Multiple named pools require ordered values T5099 - ipoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', ipoe)) + ipoe['ordered_named_pools'] = get_pools_in_order( + dict_search('client_ip_pool', ipoe) + ) ipoe['server_type'] = 'ipoe' return ipoe @@ -68,11 +71,23 @@ def verify(ipoe): for interface, iface_config in ipoe['interface'].items(): verify_interface_exists(ipoe, interface, warning_only=True) if 'client_subnet' in iface_config and 'vlan' in iface_config: - raise ConfigError('Option "client-subnet" and "vlan" are mutually exclusive, ' - 'use "client-ip-pool" instead!') - if 'vlan_mon' in iface_config and not 'vlan' in iface_config: + raise ConfigError( + 'Options "client-subnet" and "vlan" are mutually exclusive, ' + 'use "client-ip-pool" instead!' + ) + if 'vlan_mon' in iface_config and 'vlan' not in iface_config: raise ConfigError('Option "vlan-mon" requires "vlan" to be set!') + if 'lua_username' in iface_config: + if 'lua_file' not in ipoe: + raise ConfigError( + 'Option "lua-username" requires "lua-file" to be set!' + ) + if dict_search('authentication.mode', ipoe) != 'radius': + raise ConfigError( + 'Can configure username with Lua script only for RADIUS authentication' + ) + verify_accel_ppp_authentication(ipoe, local_users=False) verify_accel_ppp_ip_pool(ipoe) verify_accel_ppp_name_servers(ipoe) @@ -88,14 +103,15 @@ def generate(ipoe): render(ipoe_conf, 'accel-ppp/ipoe.config.j2', ipoe) if dict_search('authentication.mode', ipoe) == 'local': - render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', - ipoe, permission=0o640) + render( + ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.j2', ipoe, permission=0o640 + ) return None def apply(ipoe): systemd_service = 'accel-ppp@ipoe.service' - if ipoe == None: + if ipoe is None: call(f'systemctl stop {systemd_service}') for file in [ipoe_conf, ipoe_chap_secrets]: if os.path.exists(file): @@ -103,7 +119,10 @@ def apply(ipoe): return None - call(f'systemctl reload-or-restart {systemd_service}') + # Accel-pppd does not do soft-reload correctly. + # Most of the changes require restarting the service + call(f'systemctl restart {systemd_service}') + if __name__ == '__main__': try: diff --git a/src/init/vyos-router b/src/init/vyos-router index 8825cc16a..f8cc87507 100755 --- a/src/init/vyos-router +++ b/src/init/vyos-router @@ -471,6 +471,12 @@ start () touch /tmp/vyos.smoketest.debug fi + # Cleanup PKI CAs + if [ -d /usr/local/share/ca-certificates/vyos ]; then + rm -f /usr/local/share/ca-certificates/vyos/*.crt + update-ca-certificates >/dev/null 2>&1 + fi + log_action_begin_msg "Mounting VyOS Config" # ensure the vyatta_configdir supports a large number of inodes since # the config hierarchy is often inode-bound (instead of size). diff --git a/src/migration-scripts/qos/2-to-3 b/src/migration-scripts/qos/2-to-3 new file mode 100644 index 000000000..284fe828e --- /dev/null +++ b/src/migration-scripts/qos/2-to-3 @@ -0,0 +1,34 @@ +# 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/>. + +from vyos.configtree import ConfigTree + + +def migrate(config: ConfigTree) -> None: + base = ['qos', 'policy', 'cake'] + if config.exists(base): + for policy in config.list_nodes(base): + if config.exists(base + [policy, 'flow-isolation']): + isolation = None + for isol in config.list_nodes(base + [policy, 'flow-isolation']): + if isol == 'nat': + config.set(base + [policy, 'flow-isolation-nat']) + else: + isolation = isol + + config.delete(base + [policy, 'flow-isolation']) + + if isolation: + config.set(base + [policy, 'flow-isolation'], value=isolation) diff --git a/src/op_mode/show_configuration_files.sh b/src/op_mode/show_configuration_files.sh deleted file mode 100755 index ad8e0747c..000000000 --- a/src/op_mode/show_configuration_files.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -# Wrapper script for the show configuration files command -find ${vyatta_sysconfdir}/config/ \ - -type f \ - -not -name ".*" \ - -not -name "config.boot.*" \ - -printf "%f\t(%Tc)\t%T@\n" \ - | sort -r -k3 \ - | awk -F"\t" '{printf ("%-20s\t%s\n", $1,$2) ;}' diff --git a/src/services/vyos-configd b/src/services/vyos-configd index cb23642dc..d977ba2cb 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -56,6 +56,7 @@ else: SOCKET_PATH = 'ipc:///run/vyos-configd.sock' MAX_MSG_SIZE = 65535 +PAD_MSG_SIZE = 6 # Response error codes R_SUCCESS = 1 @@ -256,25 +257,14 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]: def send_result(sock, err, msg): - msg_size = min(MAX_MSG_SIZE, len(msg)) if msg else 0 - - err_rep = err.to_bytes(1, byteorder=sys.byteorder) - logger.debug(f'Sending reply: {err}') - sock.send(err_rep) - - # size req from vyshim client - size_req = sock.recv().decode() - logger.debug(f'Received request: {size_req}') - msg_size_rep = hex(msg_size).encode() - sock.send(msg_size_rep) - logger.debug(f'Sending reply: {msg_size}') - - if msg_size > 0: - # send req is sent from vyshim client only if msg_size > 0 - send_req = sock.recv().decode() - logger.debug(f'Received request: {send_req}') - sock.send(msg.encode()) - logger.debug('Sending reply with output') + msg = msg if msg else '' + msg_size = min(MAX_MSG_SIZE, len(msg)) + + err_rep = err.to_bytes(1) + msg_size_rep = f'{msg_size:#0{PAD_MSG_SIZE}x}' + + logger.debug(f'Sending reply: error_code {err} with output') + sock.send_multipart([err_rep, msg_size_rep.encode(), msg.encode()]) write_stdout_log(script_stdout_log, msg) diff --git a/src/shim/vyshim.c b/src/shim/vyshim.c index 68e6c4015..1eb653cbf 100644 --- a/src/shim/vyshim.c +++ b/src/shim/vyshim.c @@ -119,21 +119,17 @@ int main(int argc, char* argv[]) zmq_send(requester, string_node_data_msg, strlen(string_node_data_msg), 0); zmq_recv(requester, error_code, 1, 0); - debug_print("Received node data receipt\n"); + debug_print("Received node data receipt with error_code\n"); char msg_size_str[7]; - zmq_send(requester, "msg_size", 8, 0); zmq_recv(requester, msg_size_str, 6, 0); msg_size_str[6] = '\0'; int msg_size = (int)strtol(msg_size_str, NULL, 16); debug_print("msg_size: %d\n", msg_size); - if (msg_size > 0) { - zmq_send(requester, "send", 4, 0); - char *msg = s_recv_string(requester, msg_size); - printf("%s", msg); - free(msg); - } + char *msg = s_recv_string(requester, msg_size); + printf("%s", msg); + free(msg); free(string_node_data_msg); |