diff options
57 files changed, 582 insertions, 118 deletions
diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 3c0d47b27..6df12db2c 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -128,10 +128,16 @@ bind={{ radius_source_address }} {% if radius_dynamic_author %} dae-server={{ radius_dynamic_author.server }}:{{ radius_dynamic_author.port }},{{ radius_dynamic_author.key }} {% endif %} -{% if radius_shaper_attr %} + +{% if radius_shaper_enable %} [shaper] verbose=1 +{% if radius_shaper_attr %} attr={{ radius_shaper_attr }} +{% endif %} +{% if radius_shaper_multiplier %} +rate-multiplier={{ radius_shaper_multiplier }} +{% endif %} {% if radius_shaper_vendor %} vendor={{ radius_shaper_vendor }} {% endif %} diff --git a/data/templates/ntp/ntpd.conf.j2 b/data/templates/ntp/ntpd.conf.j2 index da610051e..8921826fa 100644 --- a/data/templates/ntp/ntpd.conf.j2 +++ b/data/templates/ntp/ntpd.conf.j2 @@ -33,10 +33,17 @@ restrict {{ address | address_from_cidr }} mask {{ address | netmask_from_cidr } {% endfor %} {% endif %} -{% if listen_address %} +{% if listen_address is vyos_defined or interface is vyos_defined %} # NTP should listen on configured addresses only interface ignore wildcard -{% for address in listen_address %} +{% if listen_address is vyos_defined %} +{% for address in listen_address %} interface listen {{ address }} -{% endfor %} +{% endfor %} +{% endif %} +{% if interface is vyos_defined %} +{% for ifname in interface %} +interface listen {{ ifname }} +{% endfor %} +{% endif %} {% endif %} diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2 index 6902dc05a..ed15b32f0 100644 --- a/data/templates/router-advert/radvd.conf.j2 +++ b/data/templates/router-advert/radvd.conf.j2 @@ -55,6 +55,9 @@ interface {{ iface }} { {% endif %} {% if iface_config.name_server is vyos_defined %} RDNSS {{ iface_config.name_server | join(" ") }} { +{% if iface_config.name_server_lifetime is vyos_defined %} + AdvRDNSSLifetime {{ iface_config.name_server_lifetime }}; +{% endif %} }; {% endif %} {% if iface_config.dnssl is vyos_defined %} diff --git a/debian/vyos-1x-smoketest.install b/debian/vyos-1x-smoketest.install index 3739763b9..406fef4be 100644 --- a/debian/vyos-1x-smoketest.install +++ b/debian/vyos-1x-smoketest.install @@ -1,4 +1,5 @@ usr/bin/vyos-smoketest usr/bin/vyos-configtest +usr/bin/vyos-configtest-pki usr/libexec/vyos/tests/smoke usr/libexec/vyos/tests/config diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index 12dc11de5..baff4a841 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -133,8 +133,12 @@ <format>@</format> <description>Root record</description> </valueHelp> + <valueHelp> + <format>any</format> + <description>Wildcard record (any subdomain)</description> + </valueHelp> <constraint> - <regex>([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)</regex> + <regex>([-_a-zA-Z0-9.]{1,63}|@|any)(?<!\.)</regex> </constraint> </properties> <children> @@ -166,8 +170,12 @@ <format>@</format> <description>Root record</description> </valueHelp> + <valueHelp> + <format>any</format> + <description>Wildcard record (any subdomain)</description> + </valueHelp> <constraint> - <regex>([-_a-zA-Z0-9.]{1,63}|@)(?<!\.)</regex> + <regex>([-_a-zA-Z0-9.]{1,63}|@|any)(?<!\.)</regex> </constraint> </properties> <children> diff --git a/interface-definitions/include/firewall/geoip.xml.i b/interface-definitions/include/firewall/geoip.xml.i index f6208f718..9fb37a574 100644 --- a/interface-definitions/include/firewall/geoip.xml.i +++ b/interface-definitions/include/firewall/geoip.xml.i @@ -17,6 +17,12 @@ <multi /> </properties> </leafNode> + <leafNode name="inverse-match"> + <properties> + <help>Inverse match of country-codes</help> + <valueless/> + </properties> + </leafNode> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/generic-interface-multi.xml.i b/interface-definitions/include/generic-interface-multi.xml.i index 44e87775c..65aae28ae 100644 --- a/interface-definitions/include/generic-interface-multi.xml.i +++ b/interface-definitions/include/generic-interface-multi.xml.i @@ -1,7 +1,7 @@ <!-- include start from generic-interface-multi.xml.i --> <leafNode name="interface"> <properties> - <help>Interface Name to use</help> + <help>Interface to use</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> diff --git a/interface-definitions/include/generic-interface.xml.i b/interface-definitions/include/generic-interface.xml.i index 50af718a5..8b4cf1d65 100644 --- a/interface-definitions/include/generic-interface.xml.i +++ b/interface-definitions/include/generic-interface.xml.i @@ -1,7 +1,7 @@ <!-- include start from generic-interface.xml.i --> <leafNode name="interface"> <properties> - <help>Interface Name to use</help> + <help>Interface to use</help> <completionHelp> <script>${vyos_completion_dir}/list_interfaces.py</script> </completionHelp> diff --git a/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i b/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i index b9dd59bea..5057ed9ae 100644 --- a/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i +++ b/interface-definitions/include/interface/address-ipv4-ipv6-dhcp.xml.i @@ -1,4 +1,4 @@ -<!-- include start from address-ipv4-ipv6-dhcp.xml.i --> +<!-- include start from interface/address-ipv4-ipv6-dhcp.xml.i --> <leafNode name="address"> <properties> <help>IP address</help> diff --git a/interface-definitions/include/interface/address-ipv4-ipv6.xml.i b/interface-definitions/include/interface/address-ipv4-ipv6.xml.i index 519622050..d689da5aa 100644 --- a/interface-definitions/include/interface/address-ipv4-ipv6.xml.i +++ b/interface-definitions/include/interface/address-ipv4-ipv6.xml.i @@ -1,4 +1,4 @@ -<!-- include start from address-ipv4-ipv6.xml.i --> +<!-- include start from interface/address-ipv4-ipv6.xml.i --> <leafNode name="address"> <properties> <help>IP address</help> diff --git a/interface-definitions/include/pki/ca-certificate-multi.xml.i b/interface-definitions/include/pki/ca-certificate-multi.xml.i new file mode 100644 index 000000000..646131b54 --- /dev/null +++ b/interface-definitions/include/pki/ca-certificate-multi.xml.i @@ -0,0 +1,15 @@ +<!-- include start from pki/ca-certificate-multi.xml.i --> +<leafNode name="ca-certificate"> + <properties> + <help>Certificate Authority chain in PKI configuration</help> + <completionHelp> + <path>pki ca</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Name of CA in PKI configuration</description> + </valueHelp> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/interfaces-bridge.xml.in b/interface-definitions/interfaces-bridge.xml.in index 60edf3ce2..48ee1efbc 100644 --- a/interface-definitions/interfaces-bridge.xml.in +++ b/interface-definitions/interfaces-bridge.xml.in @@ -73,12 +73,18 @@ </leafNode> <node name="igmp"> <properties> - <help>Internet Group Management Protocol (IGMP) settings</help> + <help>Internet Group Management Protocol (IGMP) and Multicast Listener Discovery (MLD) settings</help> </properties> <children> <leafNode name="querier"> <properties> - <help>Enable IGMP querier</help> + <help>Enable IGMP/MLD querier</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="snooping"> + <properties> + <help>Enable IGMP/MLD snooping</help> <valueless/> </properties> </leafNode> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index f1cbf8468..6cbd91ff4 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -741,7 +741,7 @@ </properties> </leafNode> #include <include/pki/certificate.xml.i> - #include <include/pki/ca-certificate.xml.i> + #include <include/pki/ca-certificate-multi.xml.i> <leafNode name="dh-params"> <properties> <help>Diffie Hellman parameters (server only)</help> diff --git a/interface-definitions/interfaces-pppoe.xml.in b/interface-definitions/interfaces-pppoe.xml.in index 664914baa..9674cfc0e 100644 --- a/interface-definitions/interfaces-pppoe.xml.in +++ b/interface-definitions/interfaces-pppoe.xml.in @@ -4,7 +4,7 @@ <children> <tagNode name="pppoe" owner="${vyos_conf_scripts_dir}/interfaces-pppoe.py"> <properties> - <help>Point-to-Point Protocol over Ethernet (PPPoE)</help> + <help>Point-to-Point Protocol over Ethernet (PPPoE) Interface</help> <priority>322</priority> <constraint> <regex>pppoe[0-9]+</regex> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 6b62f4c61..53e6445fa 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -4,7 +4,7 @@ <children> <tagNode name="pseudo-ethernet" owner="${vyos_conf_scripts_dir}/interfaces-pseudo-ethernet.py"> <properties> - <help>Pseudo Ethernet</help> + <help>Pseudo Ethernet Interface (Macvlan)</help> <priority>321</priority> <constraint> <regex>peth[0-9]+</regex> diff --git a/interface-definitions/interfaces-vti.xml.in b/interface-definitions/interfaces-vti.xml.in index b471c3b92..aa83a04b2 100644 --- a/interface-definitions/interfaces-vti.xml.in +++ b/interface-definitions/interfaces-vti.xml.in @@ -4,7 +4,7 @@ <children> <tagNode name="vti" owner="${vyos_conf_scripts_dir}/interfaces-vti.py"> <properties> - <help>Virtual Tunnel interface</help> + <help>Virtual Tunnel Interface (XFRM)</help> <priority>381</priority> <constraint> <regex>vti[0-9]+</regex> @@ -16,19 +16,7 @@ </valueHelp> </properties> <children> - <leafNode name="address"> - <properties> - <help>IP address</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4-host"/> - </constraint> - <multi/> - </properties> - </leafNode> + #include <include/interface/address-ipv4-ipv6.xml.i> #include <include/interface/description.xml.i> #include <include/interface/disable.xml.i> #include <include/interface/ipv4-options.xml.i> diff --git a/interface-definitions/ntp.xml.in b/interface-definitions/ntp.xml.in index a518a9def..85636a50f 100644 --- a/interface-definitions/ntp.xml.in +++ b/interface-definitions/ntp.xml.in @@ -81,6 +81,7 @@ </leafNode> </children> </node> + #include <include/generic-interface-multi.xml.i> #include <include/listen-address.xml.i> #include <include/interface/vrf.xml.i> </children> diff --git a/interface-definitions/service_conntrack-sync.xml.in b/interface-definitions/service-conntrack-sync.xml.in index 6fa6fc5f9..6fa6fc5f9 100644 --- a/interface-definitions/service_conntrack-sync.xml.in +++ b/interface-definitions/service-conntrack-sync.xml.in diff --git a/interface-definitions/service_console-server.xml.in b/interface-definitions/service-console-server.xml.in index e9591ad87..e9591ad87 100644 --- a/interface-definitions/service_console-server.xml.in +++ b/interface-definitions/service-console-server.xml.in diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index e222467b1..cd3aa3638 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -213,6 +213,11 @@ </tagNode> </children> </tagNode> + <node name="radius"> + <children> + #include <include/accel-ppp/radius-additions-rate-limit.xml.i> + </children> + </node> #include <include/radius-server-ipv4.xml.i> #include <include/accel-ppp/radius-additions.xml.i> </children> diff --git a/interface-definitions/service_mdns-repeater.xml.in b/interface-definitions/service-mdns-repeater.xml.in index 9a94f1488..9a94f1488 100644 --- a/interface-definitions/service_mdns-repeater.xml.in +++ b/interface-definitions/service-mdns-repeater.xml.in diff --git a/interface-definitions/service_monitoring_telegraf.xml.in b/interface-definitions/service-monitoring-telegraf.xml.in index d0d9202c1..d0d9202c1 100644 --- a/interface-definitions/service_monitoring_telegraf.xml.in +++ b/interface-definitions/service-monitoring-telegraf.xml.in diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service-pppoe-server.xml.in index 50f42849b..50f42849b 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service-pppoe-server.xml.in diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service-router-advert.xml.in index 40dac23ca..258b7b749 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service-router-advert.xml.in @@ -136,6 +136,23 @@ </children> </node> #include <include/name-server-ipv6.xml.i> + <leafNode name="name-server-lifetime"> + <properties> + <help>Maximum duration how long the RDNSS entries are used</help> + <valueHelp> + <format>u32:0</format> + <description>Name-servers should no longer be used</description> + </valueHelp> + <valueHelp> + <format>u32:1-7200</format> + <description>Maximum interval in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-7200"/> + </constraint> + <constraintErrorMessage>Maximum interval must be between 1 and 7200 seconds</constraintErrorMessage> + </properties> + </leafNode> <leafNode name="other-config-flag"> <properties> <help>Hosts use the administered (stateful) protocol for autoconfiguration of other (non-address) information</help> diff --git a/interface-definitions/service_sla.xml.in b/interface-definitions/service-sla.xml.in index 0c4f8a591..0c4f8a591 100644 --- a/interface-definitions/service_sla.xml.in +++ b/interface-definitions/service-sla.xml.in diff --git a/interface-definitions/service_upnp.xml.in b/interface-definitions/service-upnp.xml.in index a129b7260..a129b7260 100644 --- a/interface-definitions/service_upnp.xml.in +++ b/interface-definitions/service-upnp.xml.in diff --git a/interface-definitions/service_webproxy.xml.in b/interface-definitions/service-webproxy.xml.in index e4609b699..e4609b699 100644 --- a/interface-definitions/service_webproxy.xml.in +++ b/interface-definitions/service-webproxy.xml.in diff --git a/interface-definitions/intel_qat.xml.in b/interface-definitions/system-acceleration-qat.xml.in index 812484184..812484184 100644 --- a/interface-definitions/intel_qat.xml.in +++ b/interface-definitions/system-acceleration-qat.xml.in diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn-ipsec.xml.in index d36fbb024..d36fbb024 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn-ipsec.xml.in diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index f734283e7..f734283e7 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn-openconnect.xml.in index 21b47125d..21b47125d 100644 --- a/interface-definitions/vpn_openconnect.xml.in +++ b/interface-definitions/vpn-openconnect.xml.in diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn-pptp.xml.in index 28a53acb9..28a53acb9 100644 --- a/interface-definitions/vpn_pptp.xml.in +++ b/interface-definitions/vpn-pptp.xml.in diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn-sstp.xml.in index 195d581df..195d581df 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn-sstp.xml.in diff --git a/op-mode-definitions/show-ip.xml.in b/op-mode-definitions/show-ip.xml.in index d21c38ccc..d342ac192 100644 --- a/op-mode-definitions/show-ip.xml.in +++ b/op-mode-definitions/show-ip.xml.in @@ -7,12 +7,6 @@ <help>Show IPv4 networking information</help> </properties> <children> - <node name="external"> - <properties> - <help>Show IPv4 external address</help> - </properties> - <command>${vyos_op_scripts_dir}/show_ip_external.sh</command> - </node> <node name="neighbors"> <properties> <help>Show IPv4 neighbor (ARP) table</help> diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 7d1278d0e..3e2de4c3f 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -152,7 +152,10 @@ def parse_rule(rule_conf, fw_name, rule_id, ip_name): output.append(f'{ip_name} {prefix}addr {suffix}') if dict_search_args(side_conf, 'geoip', 'country_code'): - output.append(f'{ip_name} {prefix}addr @GEOIP_CC_{fw_name}_{rule_id}') + operator = '' + if dict_search_args(side_conf, 'geoip', 'inverse_match') != None: + operator = '!=' + output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC_{fw_name}_{rule_id}') if 'mac_address' in side_conf: suffix = side_conf["mac_address"] @@ -429,22 +432,13 @@ def geoip_update(firewall, force=False): # Map country codes to set names for codes, path in dict_search_recursive(firewall, 'country_code'): + set_name = f'GEOIP_CC_{path[1]}_{path[3]}' if path[0] == 'name': - set_name = f'GEOIP_CC_{path[1]}_{path[3]}' - ipv4_sets[set_name] = [] for code in codes: - if code not in ipv4_codes: - ipv4_codes[code] = [set_name] - else: - ipv4_codes[code].append(set_n) + ipv4_codes.setdefault(code, []).append(set_name) elif path[0] == 'ipv6_name': - set_name = f'GEOIP_CC_{path[1]}_{path[3]}' - ipv6_sets[set_name] = [] for code in codes: - if code not in ipv6_codes: - ipv6_codes[code] = [set_name] - else: - ipv6_codes[code].append(set_name) + ipv6_codes.setdefault(code, []).append(set_name) if not ipv4_codes and not ipv6_codes: if force: @@ -459,11 +453,11 @@ def geoip_update(firewall, force=False): if code in ipv4_codes and ipv4: ip_range = f'{start}-{end}' if start != end else start for setname in ipv4_codes[code]: - ipv4_sets[setname].append(ip_range) + ipv4_sets.setdefault(setname, []).append(ip_range) if code in ipv6_codes and not ipv4: ip_range = f'{start}-{end}' if start != end else start for setname in ipv6_codes[code]: - ipv6_sets[setname].append(ip_range) + ipv6_sets.setdefault(setname, []).append(ip_range) render(nftables_geoip_conf, 'firewall/nftables-geoip-update.j2', { 'ipv4_sets': ipv4_sets, diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index ffd9c590f..e4db69c1f 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -90,6 +90,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', }, + 'multicast_snooping': { + 'validate': assert_boolean, + 'location': '/sys/class/net/{ifname}/bridge/multicast_snooping', + }, }} _command_set = {**Interface._command_set, **{ @@ -198,6 +202,18 @@ class BridgeIf(Interface): """ self.set_interface('multicast_querier', enable) + def set_multicast_snooping(self, enable): + """ + Enable or disable multicast snooping on the bridge. + + Use enable=1 to enable or enable=0 to disable + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').set_multicast_snooping(1) + """ + self.set_interface('multicast_snooping', enable) + def add_port(self, interface): """ Add physical interface to bridge (member port) @@ -257,6 +273,11 @@ class BridgeIf(Interface): value = '1' if 'stp' in config else '0' self.set_stp(value) + # enable or disable multicast snooping + tmp = dict_search('igmp.snooping', config) + value = '1' if (tmp != None) else '0' + self.set_multicast_snooping(value) + # enable or disable IGMP querier tmp = dict_search('igmp.querier', config) value = '1' if (tmp != None) else '0' diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index c50cd5ce9..dc99d365a 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-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 @@ -53,3 +53,7 @@ class VTIIf(Interface): self._cmd(cmd.format(**self.config)) self.set_interface('admin_state', 'down') + + def get_mac(self): + """ Get a synthetic MAC address. """ + return self.get_mac_synthetic() diff --git a/python/vyos/pki.py b/python/vyos/pki.py index fd91fc9bf..cd15e3878 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -332,6 +332,54 @@ def verify_certificate(cert, ca_cert): except InvalidSignature: return False +def verify_crl(crl, ca_cert): + # Verify CRL was signed by specified CA + if ca_cert.subject != crl.issuer: + return False + + ca_public_key = ca_cert.public_key() + try: + if isinstance(ca_public_key, rsa.RSAPublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + padding=padding.PKCS1v15(), + algorithm=crl.signature_hash_algorithm) + elif isinstance(ca_public_key, dsa.DSAPublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + algorithm=crl.signature_hash_algorithm) + elif isinstance(ca_public_key, ec.EllipticCurvePublicKeyWithSerialization): + ca_public_key.verify( + crl.signature, + crl.tbs_certlist_bytes, + signature_algorithm=ec.ECDSA(crl.signature_hash_algorithm)) + else: + return False # We cannot verify it + return True + except InvalidSignature: + return False + +def verify_ca_chain(sorted_names, pki_node): + if len(sorted_names) == 1: # Single cert, no chain + return True + + for name in sorted_names: + cert = load_certificate(pki_node[name]['certificate']) + verified = False + for ca_name in sorted_names: + if name == ca_name: + continue + ca_cert = load_certificate(pki_node[ca_name]['certificate']) + if verify_certificate(cert, ca_cert): + verified = True + break + if not verified and name != sorted_names[-1]: + # Only permit top-most certificate to fail verify (e.g. signed by public CA not explicitly in chain) + return False + return True + # Certificate chain def find_parent(cert, ca_certs): @@ -357,3 +405,16 @@ def find_chain(cert, ca_certs): chain.append(parent) return chain + +def sort_ca_chain(ca_names, pki_node): + def ca_cmp(ca_name1, ca_name2, pki_node): + cert1 = load_certificate(pki_node[ca_name1]['certificate']) + cert2 = load_certificate(pki_node[ca_name2]['certificate']) + + if verify_certificate(cert1, cert2): # cert1 is child of cert2 + return -1 + return 1 + + from functools import cmp_to_key + return sorted(ca_names, key=cmp_to_key(lambda cert1, cert2: ca_cmp(cert1, cert2, pki_node))) + diff --git a/python/vyos/util.py b/python/vyos/util.py index 0d62fbfe9..bee5d7aec 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -197,7 +197,7 @@ def read_file(fname, defaultonfailure=None): return defaultonfailure raise e -def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None): +def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=None, append=False): """ Write content of data to given fname, should defaultonfailure be not None, it is returned on failure to read. @@ -212,7 +212,7 @@ def write_file(fname, data, defaultonfailure=None, user=None, group=None, mode=N try: """ Write a file to string """ bytes = 0 - with open(fname, 'w') as f: + with open(fname, 'w' if not append else 'a') as f: bytes = f.write(data) chown(fname, user, group) chmod(fname, mode) diff --git a/smoketest/bin/vyos-configtest-pki b/smoketest/bin/vyos-configtest-pki new file mode 100755 index 000000000..2f8af0e61 --- /dev/null +++ b/smoketest/bin/vyos-configtest-pki @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022, VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from os import system +from vyos.pki import create_private_key +from vyos.pki import create_certificate_request +from vyos.pki import create_certificate +from vyos.pki import create_certificate_revocation_list +from vyos.pki import create_dh_parameters +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key + +subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos'} +ca_subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos CA'} +subca_subject = {'country': 'DE', 'state': 'BY', 'locality': 'Cloud', 'organization': 'VyOS', 'common_name': 'vyos SubCA'} + +ca_cert = '/config/auth/ovpn_test_ca.pem' +ca_key = '/config/auth/ovpn_test_ca.key' +ca_cert_chain = '/config/auth/ovpn_test_chain.pem' +ca_crl = '/config/auth/ovpn_test_ca.crl' +subca_cert = '/config/auth/ovpn_test_subca.pem' +subca_csr = '/tmp/subca.csr' +subca_key = '/config/auth/ovpn_test_subca.key' +ssl_cert = '/config/auth/ovpn_test_server.pem' +ssl_key = '/config/auth/ovpn_test_server.key' +dh_pem = '/config/auth/ovpn_test_dh.pem' +s2s_key = '/config/auth/ovpn_test_site2site.key' +auth_key = '/config/auth/ovpn_test_tls_auth.key' + +def create_cert(subject, cert_path, key_path, sign_by=None, sign_by_key=None, ca=False, sub_ca=False): + priv_key = create_private_key('rsa', 2048) + cert_req = create_certificate_request(subject, priv_key) + cert = create_certificate( + cert_req, + sign_by if sign_by else cert_req, + sign_by_key if sign_by_key else priv_key, + is_ca=ca, is_sub_ca=sub_ca) + + with open(cert_path, 'w') as f: + f.write(encode_certificate(cert)) + + with open(key_path, 'w') as f: + f.write(encode_private_key(priv_key)) + + return cert, priv_key + +def create_empty_crl(crl_path, sign_by, sign_by_key): + crl = create_certificate_revocation_list(sign_by, sign_by_key, [1]) + + with open(crl_path, 'w') as f: + f.write(encode_certificate(crl)) + + return crl + +if __name__ == '__main__': + # Create Root CA + ca_cert_obj, ca_key_obj = create_cert(ca_subject, ca_cert, ca_key, ca=True) + + # Create Empty CRL + create_empty_crl(ca_crl, ca_cert_obj, ca_key_obj) + + # Create Intermediate CA + subca_cert_obj, subca_key_obj = create_cert( + subca_subject, subca_cert, subca_key, + sign_by=ca_cert_obj, sign_by_key=ca_key_obj, + ca=True, sub_ca=True) + + # Create Chain + with open(ca_cert_chain, 'w') as f: + f.write(encode_certificate(subca_cert_obj) + "\n") + f.write(encode_certificate(ca_cert_obj) + "\n") + + # Create Server Cert + create_cert(subject, ssl_cert, ssl_key, sign_by=subca_cert_obj, sign_by_key=subca_key_obj) + + # Create DH params + dh_params = create_dh_parameters() + + with open(dh_pem, 'w') as f: + f.write(encode_dh_parameters(dh_params)) + + # OpenVPN S2S Key + system(f'openvpn --genkey secret {s2s_key}') + + # OpenVPN Auth Key + system(f'openvpn --genkey secret {auth_key}') diff --git a/smoketest/configs/dialup-router-medium-vpn b/smoketest/configs/dialup-router-medium-vpn index 63d955738..56722d222 100644 --- a/smoketest/configs/dialup-router-medium-vpn +++ b/smoketest/configs/dialup-router-medium-vpn @@ -120,8 +120,9 @@ interfaces { persistent-tunnel remote-host 192.0.2.10 tls { - ca-cert-file /config/auth/ovpn_test_ca.pem + ca-cert-file /config/auth/ovpn_test_chain.pem cert-file /config/auth/ovpn_test_server.pem + crl-file /config/auth/ovpn_test_ca.crl key-file /config/auth/ovpn_test_server.key auth-file /config/auth/ovpn_test_tls_auth.key } @@ -152,7 +153,7 @@ interfaces { remote-host 01.foo.com remote-port 1194 tls { - ca-cert-file /config/auth/ovpn_test_ca.pem + ca-cert-file /config/auth/ovpn_test_chain.pem auth-file /config/auth/ovpn_test_tls_auth.key } } diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 816ba6dcd..8acf52243 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -232,6 +232,9 @@ class BasicInterfaceTest: for interface in self._interfaces: base = self._base_path + [interface] + # just set the interface base without any option - some interfaces + # (VTI) do not require any option to be brought up + self.cli_set(base) for option in self._options.get(interface, []): self.cli_set(base + option.split()) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index ce06b9074..4de90e1ec 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -69,8 +69,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip filter', inverse=True) - def verify_nftables(self, nftables_search, table, inverse=False): - nftables_output = cmd(f'sudo nft list table {table}') + def verify_nftables(self, nftables_search, table, inverse=False, args=''): + nftables_output = cmd(f'sudo nft {args} list table {table}') for search in nftables_search: matched = False @@ -80,6 +80,24 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): break self.assertTrue(not matched if inverse else matched, msg=search) + def test_geoip(self): + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'action', 'drop']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'action', 'accept']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr']) + self.cli_set(['firewall', 'name', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match']) + + self.cli_commit() + + nftables_search = [ + ['ip saddr @GEOIP_CC_smoketest_1', 'drop'], + ['ip saddr != @GEOIP_CC_smoketest_2', 'return'] + ] + # -t prevents 1000+ GeoIP elements being returned + self.verify_nftables(nftables_search, 'ip filter', args='-t') + def test_groups(self): hostmap_path = ['system', 'static-host-mapping', 'host-name'] example_org = ['192.0.2.8', '192.0.2.10', '192.0.2.11'] diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index ca0ead9e8..26d3a23c9 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -86,9 +86,83 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # validate member interface configuration for member in self._members: tmp = get_interface_config(member) + # verify member is assigned to the bridge + self.assertEqual(interface, tmp['master']) # Isolated must be enabled as configured above self.assertTrue(tmp['linkinfo']['info_slave_data']['isolated']) + def test_igmp_querier_snooping(self): + # Add member interfaces to bridge + for interface in self._interfaces: + base = self._base_path + [interface] + + # assign members to bridge interface + for member in self._members: + base_member = base + ['member', 'interface', member] + self.cli_set(base_member) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP default configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '0') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '0') + + # Enable IGMP snooping + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['igmp', 'snooping']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP snooping configuration + # Verify IGMP default configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '1') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '0') + + # Enable IGMP querieer + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_set(base + ['igmp', 'querier']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP snooping & querier configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '1') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '1') + + # Disable IGMP + for interface in self._interfaces: + base = self._base_path + [interface] + self.cli_delete(base + ['igmp']) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + # Verify IGMP snooping & querier configuration + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_snooping') + self.assertEqual(tmp, '0') + tmp = read_file(f'/sys/class/net/{interface}/bridge/multicast_querier') + self.assertEqual(tmp, '0') + + # validate member interface configuration + for member in self._members: + tmp = get_interface_config(member) + # verify member is assigned to the bridge + self.assertEqual(interface, tmp['master']) + def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py new file mode 100755 index 000000000..9cbf104f0 --- /dev/null +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2022 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_interfaces_test import BasicInterfaceTest + +class VTIInterfaceTest(BasicInterfaceTest.TestCase): + @classmethod + def setUpClass(cls): + cls._test_ip = True + cls._test_ipv6 = True + cls._test_mtu = True + cls._base_path = ['interfaces', 'vti'] + cls._interfaces = ['vti10', 'vti20', 'vti30'] + + # call base-classes classmethod + super(VTIInterfaceTest, cls).setUpClass() + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 4875fb5d1..1168c05cd 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -17,6 +17,7 @@ import re import unittest +from vyos.configsession import ConfigSessionError from base_vyostest_shim import VyOSUnitTestSHIM from vyos.util import read_file @@ -93,6 +94,7 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): def test_dns(self): nameserver = ['2001:db8::1', '2001:db8::2'] dnssl = ['vyos.net', 'vyos.io'] + ns_lifetime = '599' self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) self.cli_set(base_path + ['other-config-flag']) @@ -102,6 +104,14 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): for sl in dnssl: self.cli_set(base_path + ['dnssl', sl]) + self.cli_set(base_path + ['name-server-lifetime', ns_lifetime]) + # The value, if not 0, must be at least interval max (defaults to 600). + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + ns_lifetime = '600' + self.cli_set(base_path + ['name-server-lifetime', ns_lifetime]) + # commit changes self.cli_commit() @@ -110,8 +120,12 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): tmp = 'RDNSS ' + ' '.join(nameserver) + ' {' self.assertIn(tmp, config) + tmp = f'AdvRDNSSLifetime {ns_lifetime};' + self.assertIn(tmp, config) + tmp = 'DNSSL ' + ' '.join(dnssl) + ' {' self.assertIn(tmp, config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_ntp.py b/smoketest/scripts/cli/test_system_ntp.py index e2821687c..a0806acf0 100755 --- a/smoketest/scripts/cli/test_system_ntp.py +++ b/smoketest/scripts/cli/test_system_ntp.py @@ -108,5 +108,22 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for listen in listen_address: self.assertIn(f'interface listen {listen}', config) + def test_03_ntp_interface(self): + interfaces = ['eth0', 'eth1'] + for interface in interfaces: + self.cli_set(base_path + ['interface', interface]) + + servers = ['time1.vyos.net', 'time2.vyos.net'] + for server in servers: + self.cli_set(base_path + ['server', server]) + + self.cli_commit() + + # Check generated client address configuration + config = read_file(NTP_CONF) + self.assertIn('interface ignore wildcard', config) + for interface in interfaces: + self.assertIn(f'interface listen {interface}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index f1c2d1f43..41023c135 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -98,6 +98,9 @@ def get_config(config=None): dns['authoritative_zone_errors'].append('{}.{}: at least one address is required'.format(subnode, node)) continue + if subnode == 'any': + subnode = '*' + for address in rdata['address']: zone['records'].append({ 'name': subnode, diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 4750ca3e8..280a62b9a 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -39,6 +39,8 @@ from vyos.configverify import verify_mirror_redirect from vyos.ifconfig import VTunIf from vyos.pki import load_dh_parameters from vyos.pki import load_private_key +from vyos.pki import sort_ca_chain +from vyos.pki import verify_ca_chain from vyos.pki import wrap_certificate from vyos.pki import wrap_crl from vyos.pki import wrap_dh_parameters @@ -148,8 +150,14 @@ def verify_pki(openvpn): if 'ca_certificate' not in tls: raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') - if tls['ca_certificate'] not in pki['ca']: - raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + for ca_name in tls['ca_certificate']: + if ca_name not in pki['ca']: + raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + + if len(tls['ca_certificate']) > 1: + sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca']) + if not verify_ca_chain(sorted_chain, pki['ca']): + raise ConfigError(f'CA certificates are not a valid chain') if mode != 'client' and 'auth_key' not in tls: if 'certificate' not in tls: @@ -516,21 +524,28 @@ def generate_pki_files(openvpn): if tls: if 'ca_certificate' in tls: - cert_name = tls['ca_certificate'] - pki_ca = pki['ca'][cert_name] + cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') + crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') - if 'certificate' in pki_ca: - cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') - write_file(cert_path, wrap_certificate(pki_ca['certificate']), - user=user, group=group, mode=0o600) + if os.path.exists(cert_path): + os.unlink(cert_path) + + if os.path.exists(crl_path): + os.unlink(crl_path) + + for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']): + pki_ca = pki['ca'][cert_name] + + if 'certificate' in pki_ca: + write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n", + user=user, group=group, mode=0o600, append=True) - if 'crl' in pki_ca: - for crl in pki_ca['crl']: - crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') - write_file(crl_path, wrap_crl(crl), user=user, group=group, - mode=0o600) + if 'crl' in pki_ca: + for crl in pki_ca['crl']: + write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group, + mode=0o600, append=True) - openvpn['tls']['crl'] = True + openvpn['tls']['crl'] = True if 'certificate' in tls: cert_name = tls['certificate'] diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index 0d6ec9ace..5490a794d 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,9 +18,11 @@ import os from vyos.config import Config from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.configverify import verify_interface_exists from vyos.util import call +from vyos.util import get_interface_config from vyos.template import render +from vyos import ConfigError from vyos import airbag airbag.enable() @@ -49,6 +51,20 @@ def verify(ntp): raise ConfigError('NTP server not configured') verify_vrf(ntp) + + if 'interface' in ntp: + # If ntpd should listen on a given interface, ensure it exists + for interface in ntp['interface']: + verify_interface_exists(interface) + + # If we run in a VRF, our interface must belong to this VRF, too + if 'vrf' in ntp: + tmp = get_interface_config(interface) + vrf_name = ntp['vrf'] + if 'master' not in tmp or tmp['master'] != vrf_name: + raise ConfigError(f'NTP runs in VRF "{vrf_name}" - "{interface}" '\ + f'does not belong to this VRF!') + return None def generate(ntp): diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index cd46cbcb4..01f14df61 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -19,6 +19,7 @@ import os from sys import exit from sys import argv +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_prefix_list @@ -198,6 +199,9 @@ def verify(bgp): if 'source_interface' in peer_config['interface']: raise ConfigError(f'"source-interface" option not allowed for neighbor "{peer}"') + if 'address_family' not in peer_config: + Warning(f'BGP neighbor "{peer}" requires address-family!') + for afi in ['ipv4_unicast', 'ipv4_multicast', 'ipv4_labeled_unicast', 'ipv4_flowspec', 'ipv6_unicast', 'ipv6_multicast', 'ipv6_labeled_unicast', 'ipv6_flowspec', 'l2vpn_evpn']: diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 559d1bcd5..61f484129 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -53,6 +53,8 @@ default_config_data = { 'radius_nas_ip': '', 'radius_source_address': '', 'radius_shaper_attr': '', + 'radius_shaper_enable': False, + 'radius_shaper_multiplier': '', 'radius_shaper_vendor': '', 'radius_dynamic_author': '', 'thread_cnt': get_half_cpus() @@ -196,6 +198,18 @@ def get_config(config=None): if conf.exists(['nas-ip-address']): ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) + if conf.exists(['rate-limit', 'attribute']): + ipoe['radius_shaper_attr'] = conf.return_value(['rate-limit', 'attribute']) + + if conf.exists(['rate-limit', 'enable']): + ipoe['radius_shaper_enable'] = True + + if conf.exists(['rate-limit', 'multiplier']): + ipoe['radius_shaper_multiplier'] = conf.return_value(['rate-limit', 'multiplier']) + + if conf.exists(['rate-limit', 'vendor']): + ipoe['radius_shaper_vendor'] = conf.return_value(['rate-limit', 'vendor']) + if conf.exists(['source-address']): ipoe['radius_source_address'] = conf.return_value(['source-address']) diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 71b758399..ff7caaa84 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2021 VyOS maintainers and contributors +# Copyright (C) 2018-2022 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,7 +17,7 @@ import os from sys import exit - +from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render @@ -79,22 +79,35 @@ def verify(rtradv): if 'interface' not in rtradv: return None - for interface in rtradv['interface']: - interface = rtradv['interface'][interface] + for interface, interface_config in rtradv['interface'].items(): if 'prefix' in interface: - for prefix in interface['prefix']: - prefix = interface['prefix'][prefix] - valid_lifetime = prefix['valid_lifetime'] + for prefix, prefix_config in interface_config['prefix'].items(): + valid_lifetime = prefix_config['valid_lifetime'] if valid_lifetime == 'infinity': valid_lifetime = 4294967295 - preferred_lifetime = prefix['preferred_lifetime'] + preferred_lifetime = prefix_config['preferred_lifetime'] if preferred_lifetime == 'infinity': preferred_lifetime = 4294967295 if not (int(valid_lifetime) > int(preferred_lifetime)): raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') + if 'name_server_lifetime' in interface_config: + # man page states: + # The maximum duration how long the RDNSS entries are used for name + # resolution. A value of 0 means the nameserver must no longer be + # used. The value, if not 0, must be at least MaxRtrAdvInterval. To + # ensure stale RDNSS info gets removed in a timely fashion, this + # should not be greater than 2*MaxRtrAdvInterval. + lifetime = int(interface_config['name_server_lifetime']) + interval_max = int(interface_config['interval']['max']) + if lifetime > 0: + if lifetime < int(interval_max): + raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!') + if lifetime > 2* interval_max: + Warning(f'RDNSS lifetime should not exceed "{2 * interval_max}" which is two times "interval max"!') + return None def generate(rtradv): @@ -105,15 +118,16 @@ def generate(rtradv): return None def apply(rtradv): + systemd_service = 'radvd.service' if not rtradv: # bail out early - looks like removal from running config - call('systemctl stop radvd.service') + call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) return None - call('systemctl restart radvd.service') + call(f'systemctl reload-or-restart {systemd_service}') return None diff --git a/src/migration-scripts/interfaces/24-to-25 b/src/migration-scripts/interfaces/24-to-25 index 93ce9215f..4095f2a3e 100755 --- a/src/migration-scripts/interfaces/24-to-25 +++ b/src/migration-scripts/interfaces/24-to-25 @@ -20,6 +20,7 @@ import os import sys from vyos.configtree import ConfigTree +from vyos.pki import CERT_BEGIN from vyos.pki import load_certificate from vyos.pki import load_crl from vyos.pki import load_dh_parameters @@ -27,6 +28,7 @@ from vyos.pki import load_private_key from vyos.pki import encode_certificate from vyos.pki import encode_dh_parameters from vyos.pki import encode_private_key +from vyos.pki import verify_crl from vyos.util import run def wrapped_pem_to_config_value(pem): @@ -129,6 +131,8 @@ if config.exists(base): config.delete(base + [interface, 'tls', 'crypt-file']) + ca_certs = {} + if config.exists(x509_base + ['ca-cert-file']): if not config.exists(pki_base + ['ca']): config.set(pki_base + ['ca']) @@ -136,20 +140,27 @@ if config.exists(base): cert_file = config.return_value(x509_base + ['ca-cert-file']) cert_path = os.path.join(AUTH_DIR, cert_file) - cert = None if os.path.isfile(cert_path): if not os.access(cert_path, os.R_OK): run(f'sudo chmod 644 {cert_path}') with open(cert_path, 'r') as f: - cert_data = f.read() - cert = load_certificate(cert_data, wrap_tags=False) - - if cert: - cert_pem = encode_certificate(cert) - config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) - config.set(x509_base + ['ca-certificate'], value=pki_name) + certs_str = f.read() + certs_data = certs_str.split(CERT_BEGIN) + index = 1 + for cert_data in certs_data[1:]: + cert = load_certificate(CERT_BEGIN + cert_data, wrap_tags=False) + + if cert: + ca_certs[f'{pki_name}_{index}'] = cert + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', f'{pki_name}_{index}', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=f'{pki_name}_{index}', replace=False) + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + index += 1 else: print(f'Failed to migrate CA certificate on openvpn interface {interface}') @@ -163,6 +174,7 @@ if config.exists(base): crl_file = config.return_value(x509_base + ['crl-file']) crl_path = os.path.join(AUTH_DIR, crl_file) crl = None + crl_ca_name = None if os.path.isfile(crl_path): if not os.access(crl_path, os.R_OK): @@ -172,9 +184,14 @@ if config.exists(base): crl_data = f.read() crl = load_crl(crl_data, wrap_tags=False) - if crl: + for ca_name, ca_cert in ca_certs.items(): + if verify_crl(crl, ca_cert): + crl_ca_name = ca_name + break + + if crl and crl_ca_name: crl_pem = encode_certificate(crl) - config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + config.set(pki_base + ['ca', crl_ca_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) else: print(f'Failed to migrate CRL on openvpn interface {interface}') diff --git a/src/op_mode/show_ip_external.sh b/src/op_mode/show_ip_external.sh deleted file mode 100755 index 275d05278..000000000 --- a/src/op_mode/show_ip_external.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# Detect an external IP address -# Use random services for checking - - -array=( - ipinfo.io/ip - ifconfig.me - ipecho.net/plain - icanhazip.com - v4.ident.me - checkip.amazonaws.com -) - -size=${#array[@]} -index=$(($RANDOM % $size)) - -curl --silent ${array[$index]} | tr -d "[:space:]" && echo diff --git a/src/system/vyos-event-handler.py b/src/system/vyos-event-handler.py index 691f674b2..1c85380bc 100755 --- a/src/system/vyos-event-handler.py +++ b/src/system/vyos-event-handler.py @@ -15,14 +15,16 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse -import select -import re import json +import re +import select +from copy import deepcopy from os import getpid, environ from pathlib import Path from signal import signal, SIGTERM, SIGINT -from systemd import journal from sys import exit +from systemd import journal + from vyos.util import run, dict_search # Identify this script @@ -54,7 +56,7 @@ class Analyzer: script_arguments = dict_search('script.arguments', event_config) script = f'{script} {script_arguments}' # Prepare environment - environment = environ + environment = deepcopy(environ) # Check for additional environment options if dict_search('script.environment', event_config): for env_variable, env_value in dict_search( @@ -69,7 +71,7 @@ class Analyzer: 'pattern_raw': pattern_raw, 'syslog_id': - dict_search('filter.syslog_identifier', event_config), + dict_search('filter.syslog-identifier', event_config), 'pattern_script': { 'path': script, 'environment': environment diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service index 5cc7869cb..23cd4cfc3 100644 --- a/src/systemd/dhclient@.service +++ b/src/systemd/dhclient@.service @@ -14,7 +14,7 @@ ExecStart=/sbin/dhclient -4 $DHCLIENT_OPTS ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r Restart=always TimeoutStopSec=20 -SendSIGKILL=SIGKILL +SendSIGKILL=true FinalKillSignal=SIGABRT [Install] |