diff options
-rw-r--r-- | .github/workflows/package-smoketest.yml | 43 | ||||
-rw-r--r-- | interface-definitions/interfaces_bonding.xml.in | 1 | ||||
-rw-r--r-- | op-mode-definitions/date.xml.in | 2 | ||||
-rw-r--r-- | python/vyos/configverify.py | 17 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 72 | ||||
-rw-r--r-- | smoketest/scripts/cli/base_interfaces_test.py | 162 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_interfaces_ethernet.py | 142 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_bonding.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/interfaces_ethernet.py | 78 | ||||
-rwxr-xr-x | src/op_mode/vpn_ike_sa.py | 2 |
10 files changed, 293 insertions, 230 deletions
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml index 467ff062e..a74c35adf 100644 --- a/.github/workflows/package-smoketest.yml +++ b/.github/workflows/package-smoketest.yml @@ -15,6 +15,9 @@ permissions: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed for PR comments + BUILD_BY: autobuild@vyos.net + DEBIAN_MIRROR: http://deb.debian.org/debian/ + VYOS_MIRROR: https://rolling-packages.vyos.net/current/ jobs: build_iso: @@ -23,9 +26,6 @@ jobs: container: image: vyos/vyos-build:current options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged - env: - BUILD_BY: autobuild@vyos.net - DEBIAN_MIRROR: http://deb.debian.org/debian/ outputs: build_version: ${{ steps.version.outputs.build_version }} steps: @@ -52,9 +52,11 @@ jobs: sudo --preserve-env ./build-vyos-image \ --architecture amd64 \ --build-by $BUILD_BY \ + --build-type release \ + --custom-package vyos-1x-smoketest \ --debian-mirror $DEBIAN_MIRROR \ --version ${{ steps.version.outputs.build_version }} \ - --build-type release \ + --vyos-mirror $VYOS_MIRROR \ generic - uses: actions/upload-artifact@v4 with: @@ -154,11 +156,43 @@ jobs: echo "exit_code=fail" >> $GITHUB_OUTPUT fi + test_encrypted_config_tpm: + needs: build_iso + runs-on: ubuntu-24.04 + timeout-minutes: 30 + container: + image: vyos/vyos-build:current + options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged + outputs: + exit_code: ${{ steps.test.outputs.exit_code }} + steps: + # We need the test script from vyos-build repo + - name: Clone vyos-build source code + uses: actions/checkout@v4 + with: + repository: vyos/vyos-build + - uses: actions/download-artifact@v4 + with: + name: vyos-${{ needs.build_iso.outputs.build_version }} + path: build + - name: VyOS TPM encryption tests + id: test + shell: bash + run: | + set -e + sudo make testtpm + if [[ $? == 0 ]]; then + echo "exit_code=success" >> $GITHUB_OUTPUT + else + echo "exit_code=fail" >> $GITHUB_OUTPUT + fi + result: needs: - test_smoketest_cli - test_config_load - test_raid1_install + - test_encrypted_config_tpm runs-on: ubuntu-24.04 timeout-minutes: 5 if: always() @@ -177,6 +211,7 @@ jobs: * CLI Smoketests ${{ needs.test_smoketest_cli.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }} * Config tests ${{ needs.test_config_load.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }} * RAID1 tests ${{ needs.test_raid1_install.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }} + * TPM tests ${{ needs.test_encrypted_config_tpm.outputs.exit_code == 'success' && '👍 passed' || '❌ failed' }} message-id: "SMOKETEST_RESULTS" allow-repeats: false diff --git a/interface-definitions/interfaces_bonding.xml.in b/interface-definitions/interfaces_bonding.xml.in index cc0327f3d..b17cad478 100644 --- a/interface-definitions/interfaces_bonding.xml.in +++ b/interface-definitions/interfaces_bonding.xml.in @@ -56,6 +56,7 @@ #include <include/interface/disable.xml.i> #include <include/interface/vrf.xml.i> #include <include/interface/mirror.xml.i> + #include <include/interface/eapol.xml.i> <node name="evpn"> <properties> <help>EVPN Multihoming</help> diff --git a/op-mode-definitions/date.xml.in b/op-mode-definitions/date.xml.in index 6d8586025..4e62a8335 100644 --- a/op-mode-definitions/date.xml.in +++ b/op-mode-definitions/date.xml.in @@ -35,7 +35,7 @@ <list><MMDDhhmm> <MMDDhhmmYY> <MMDDhhmmCCYY> <MMDDhhmmCCYY.ss></list> </completionHelp> </properties> - <command>/bin/date "$3"</command> + <command>sudo bash -c "/bin/date '$3' && hwclock --systohc --localtime"</command> </tagNode> </children> </node> diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 59b67300d..92996f2ee 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -520,3 +520,20 @@ def verify_pki_dh_parameters(config: dict, dh_name: str, min_key_size: int=0): dh_bits = dh_numbers.p.bit_length() if dh_bits < min_key_size: raise ConfigError(f'Minimum DH key-size is {min_key_size} bits!') + +def verify_eapol(config: dict): + """ + Common helper function used by interface implementations to perform + recurring validation of EAPoL configuration. + """ + if 'eapol' not in config: + return + + if 'certificate' not in config['eapol']: + raise ConfigError('Certificate must be specified when using EAPoL!') + + verify_pki_certificate(config, config['eapol']['certificate'], no_password_protected=True) + + if 'ca_certificate' in config['eapol']: + for ca_cert in config['eapol']['ca_certificate']: + verify_pki_ca_certificate(config, ca_cert) diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 72d3d3afe..31fcf6ca6 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -32,6 +32,12 @@ from vyos.configdict import list_diff from vyos.configdict import dict_merge from vyos.configdict import get_vlan_ids from vyos.defaults import directories +from vyos.pki import find_chain +from vyos.pki import encode_certificate +from vyos.pki import load_certificate +from vyos.pki import wrap_private_key +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 from vyos.template import render from vyos.utils.network import mac2eui64 from vyos.utils.dict import dict_search @@ -41,9 +47,8 @@ from vyos.utils.network import get_vrf_tableid from vyos.utils.network import is_netns_interface from vyos.utils.process import is_systemd_service_active from vyos.utils.process import run -from vyos.template import is_ipv4 -from vyos.template import is_ipv6 from vyos.utils.file import read_file +from vyos.utils.file import write_file from vyos.utils.network import is_intf_addr_assigned from vyos.utils.network import is_ipv6_link_local from vyos.utils.assertion import assert_boolean @@ -52,7 +57,6 @@ from vyos.utils.assertion import assert_mac from vyos.utils.assertion import assert_mtu from vyos.utils.assertion import assert_positive from vyos.utils.assertion import assert_range - from vyos.ifconfig.control import Control from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational @@ -377,6 +381,9 @@ class Interface(Control): >>> i = Interface('eth0') >>> i.remove() """ + # Stop WPA supplicant if EAPoL was in use + if is_systemd_service_active(f'wpa_supplicant-wired@{self.ifname}'): + self._cmd(f'systemctl stop wpa_supplicant-wired@{self.ifname}') # remove all assigned IP addresses from interface - this is a bit redundant # as the kernel will remove all addresses on interface deletion, but we @@ -1522,6 +1529,61 @@ class Interface(Control): return None self.set_interface('per_client_thread', enable) + def set_eapol(self) -> None: + """ Take care about EAPoL supplicant daemon """ + + # XXX: wpa_supplicant works on the source interface + cfg_dir = '/run/wpa_supplicant' + wpa_supplicant_conf = f'{cfg_dir}/{self.ifname}.conf' + eapol_action='stop' + + if 'eapol' in self.config: + # The default is a fallback to hw_id which is not present for any interface + # other then an ethernet interface. Thus we emulate hw_id by reading back the + # Kernel assigned MAC address + if 'hw_id' not in self.config: + self.config['hw_id'] = read_file(f'/sys/class/net/{self.ifname}/address') + render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', self.config) + + cert_file_path = os.path.join(cfg_dir, f'{self.ifname}_cert.pem') + cert_key_path = os.path.join(cfg_dir, f'{self.ifname}_cert.key') + + cert_name = self.config['eapol']['certificate'] + pki_cert = self.config['pki']['certificate'][cert_name] + + loaded_pki_cert = load_certificate(pki_cert['certificate']) + loaded_ca_certs = {load_certificate(c['certificate']) + for c in self.config['pki']['ca'].values()} if 'ca' in self.config['pki'] else {} + + cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) + + write_file(cert_file_path, + '\n'.join(encode_certificate(c) for c in cert_full_chain)) + write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) + + if 'ca_certificate' in self.config['eapol']: + ca_cert_file_path = os.path.join(cfg_dir, f'{self.ifname}_ca.pem') + ca_chains = [] + + for ca_cert_name in self.config['eapol']['ca_certificate']: + pki_ca_cert = self.config['pki']['ca'][ca_cert_name] + loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) + ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) + ca_chains.append( + '\n'.join(encode_certificate(c) for c in ca_full_chain)) + + write_file(ca_cert_file_path, '\n'.join(ca_chains)) + + eapol_action='reload-or-restart' + + # start/stop WPA supplicant service + self._cmd(f'systemctl {eapol_action} wpa_supplicant-wired@{self.ifname}') + + if 'eapol' not in self.config: + # delete configuration on interface removal + if os.path.isfile(wpa_supplicant_conf): + os.unlink(wpa_supplicant_conf) + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -1609,7 +1671,6 @@ class Interface(Control): tmp = get_interface_config(config['ifname']) if 'master' in tmp and tmp['master'] != bridge_if: self.set_vrf('') - else: self.set_vrf(config.get('vrf', '')) @@ -1752,6 +1813,9 @@ class Interface(Control): value = '1' if (tmp != None) else '0' self.set_per_client_thread(value) + # enable/disable EAPoL (Extensible Authentication Protocol over Local Area Network) + self.set_eapol() + # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index e7e29387f..593b4b415 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -12,6 +12,8 @@ # 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 re + from netifaces import AF_INET from netifaces import AF_INET6 from netifaces import ifaddresses @@ -22,6 +24,7 @@ from vyos.configsession import ConfigSessionError from vyos.defaults import directories from vyos.ifconfig import Interface from vyos.ifconfig import Section +from vyos.pki import CERT_BEGIN from vyos.utils.file import read_file from vyos.utils.dict import dict_search from vyos.utils.process import cmd @@ -40,6 +43,79 @@ dhclient_process_name = 'dhclient' dhcp6c_base_dir = directories['dhcp6_client_dir'] dhcp6c_process_name = 'dhcp6c' +server_ca_root_cert_data = """ +MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa +Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj +ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK +BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW +a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ== +""" + +server_ca_intermediate_cert_data = """ +MIIBmTCCAT+gAwIBAgIUNzrtHzLmi3QpPK57tUgCnJZhXXQwCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa +Fw0zMjAyMTUxOTQxMjFaMCYxJDAiBgNVBAMMG1Z5T1Mgc2VydmVyIGludGVybWVk +aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEl2nJ1CzoqPV6hWII2m +eGN/uieU6wDMECTk/LgG8CCCSYb488dibUiFN/1UFsmoLIdIhkx/6MUCYh62m8U2 +WNujUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMV3YwH88I5gFsFUibbQ +kMR0ECPsMB8GA1UdIwQYMBaAFHAL9hqJ4wSeOFhrsrcWXD+jRIW7MAoGCCqGSM49 +BAMCA0gAMEUCIQC/ahujD9dp5pMMCd3SZddqGC9cXtOwMN0JR3e5CxP13AIgIMQm +jMYrinFoInxmX64HfshYqnUY8608nK9D2BNPOHo= +""" + +client_ca_root_cert_data = """ +MIIBcDCCARagAwIBAgIUZmoW2xVdwkZSvglnkCq0AHKa6zIwCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa +Fw0zMjAyMTUxOTQxMjFaMB4xHDAaBgNVBAMME1Z5T1MgY2xpZW50IHJvb3QgQ0Ew +WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATUpKXzQk2NOVKDN4VULk2yw4mOKPvn +mg947+VY7lbpfOfAUD0QRg95qZWCw899eKnXp/U4TkAVrmEKhUb6OJTFozIwMDAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTXu6xGWUl25X3sBtrhm3BJSICIATAK +BggqhkjOPQQDAgNIADBFAiEAnTzEwuTI9bz2Oae3LZbjP6f/f50KFJtjLZFDbQz7 +DpYCIDNRHV8zBUibC+zg5PqMpQBKd/oPfNU76nEv6xkp/ijO +""" + +client_ca_intermediate_cert_data = """ +MIIBmDCCAT+gAwIBAgIUJEMdotgqA7wU4XXJvEzDulUAGqgwCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjJa +Fw0zMjAyMTUxOTQxMjJaMCYxJDAiBgNVBAMMG1Z5T1MgY2xpZW50IGludGVybWVk +aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGyIVIi217s9j3O+WQ2b +6R65/Z0ZjQpELxPjBRc0CA0GFCo+pI5EvwI+jNFArvTAJ5+ZdEWUJ1DQhBKDDQdI +avCjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOUS8oNJjChB1Rb9Blcl +ETvziHJ9MB8GA1UdIwQYMBaAFNe7rEZZSXblfewG2uGbcElIgIgBMAoGCCqGSM49 +BAMCA0cAMEQCIArhaxWgRsAUbEeNHD/ULtstLHxw/P97qPUSROLQld53AiBjgiiz +9pDfISmpekZYz6bIDWRIR0cXUToZEMFNzNMrQg== +""" + +client_cert_data = """ +MIIBmTCCAUCgAwIBAgIUV5T77XdE/tV82Tk4Vzhp5BIFFm0wCgYIKoZIzj0EAwIw +JjEkMCIGA1UEAwwbVnlPUyBjbGllbnQgaW50ZXJtZWRpYXRlIENBMB4XDTIyMDIx +NzE5NDEyMloXDTMyMDIxNTE5NDEyMlowIjEgMB4GA1UEAwwXVnlPUyBjbGllbnQg +Y2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuyynqfc/qJj5e +KJ03oOH8X4Z8spDeAPO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAh +CIhytmJao1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTIFKrxZ+PqOhYSUqnl +TGCUmM7wTjAfBgNVHSMEGDAWgBTlEvKDSYwoQdUW/QZXJRE784hyfTAKBggqhkjO +PQQDAgNHADBEAiAvO8/jvz05xqmP3OXD53XhfxDLMIxzN4KPoCkFqvjlhQIgIHq2 +/geVx3rAOtSps56q/jiDouN/aw01TdpmGKVAa9U= +""" + +client_key_data = """ +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxaxAQsJwjoOCByQE ++qSYKtKtJzbdbOnTsKNSrfgkFH6hRANCAARuyynqfc/qJj5eKJ03oOH8X4Z8spDe +APO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAhCIhytmJa +""" + +def get_wpa_supplicant_value(interface, key): + tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') + tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) + return tmp[0] + +def get_certificate_count(interface, cert_type): + tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem') + return tmp.count(CERT_BEGIN) + def is_mirrored_to(interface, mirror_if, qdisc): """ Ask TC if we are mirroring traffic to a discrete interface. @@ -57,10 +133,10 @@ def is_mirrored_to(interface, mirror_if, qdisc): if mirror_if in tmp: ret_val = True return ret_val - class BasicInterfaceTest: class TestCase(VyOSUnitTestSHIM.TestCase): _test_dhcp = False + _test_eapol = False _test_ip = False _test_mtu = False _test_vlan = False @@ -92,6 +168,7 @@ class BasicInterfaceTest: cls._test_vlan = cli_defined(cls._base_path, 'vif') cls._test_qinq = cli_defined(cls._base_path, 'vif-s') cls._test_dhcp = cli_defined(cls._base_path, 'dhcp-options') + cls._test_eapol = cli_defined(cls._base_path, 'eapol') cls._test_ip = cli_defined(cls._base_path, 'ip') cls._test_ipv6 = cli_defined(cls._base_path, 'ipv6') cls._test_ipv6_dhcpc6 = cli_defined(cls._base_path, 'dhcpv6-options') @@ -1158,3 +1235,86 @@ class BasicInterfaceTest: # as until commit() is called, nothing happens section = Section.section(delegatee) self.cli_delete(['interfaces', section, delegatee]) + + def test_eapol(self): + if not self._test_eapol: + self.skipTest('not supported') + + cfg_dir = '/run/wpa_supplicant' + + ca_certs = { + 'eapol-server-ca-root': server_ca_root_cert_data, + 'eapol-server-ca-intermediate': server_ca_intermediate_cert_data, + 'eapol-client-ca-root': client_ca_root_cert_data, + 'eapol-client-ca-intermediate': client_ca_intermediate_cert_data, + } + cert_name = 'eapol-client' + + for name, data in ca_certs.items(): + self.cli_set(['pki', 'ca', name, 'certificate', data.replace('\n','')]) + + self.cli_set(['pki', 'certificate', cert_name, 'certificate', client_cert_data.replace('\n','')]) + self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', client_key_data.replace('\n','')]) + + for interface in self._interfaces: + path = self._base_path + [interface] + for option in self._options.get(interface, []): + self.cli_set(path + option.split()) + + # Enable EAPoL + self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-server-ca-intermediate']) + self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate']) + self.cli_set(self._base_path + [interface, 'eapol', 'certificate', cert_name]) + + self.cli_commit() + + # Test multiple CA chains + self.assertEqual(get_certificate_count(interface, 'ca'), 4) + + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate']) + + self.cli_commit() + + # Validate interface config + for interface in self._interfaces: + tmp = get_wpa_supplicant_value(interface, 'key_mgmt') + self.assertEqual('IEEE8021X', tmp) + + tmp = get_wpa_supplicant_value(interface, 'eap') + self.assertEqual('TLS', tmp) + + tmp = get_wpa_supplicant_value(interface, 'eapol_flags') + self.assertEqual('0', tmp) + + tmp = get_wpa_supplicant_value(interface, 'ca_cert') + self.assertEqual(f'"{cfg_dir}/{interface}_ca.pem"', tmp) + + tmp = get_wpa_supplicant_value(interface, 'client_cert') + self.assertEqual(f'"{cfg_dir}/{interface}_cert.pem"', tmp) + + tmp = get_wpa_supplicant_value(interface, 'private_key') + self.assertEqual(f'"{cfg_dir}/{interface}_cert.key"', tmp) + + mac = read_file(f'/sys/class/net/{interface}/address') + tmp = get_wpa_supplicant_value(interface, 'identity') + self.assertEqual(f'"{mac}"', tmp) + + # Check certificate files have the full chain + self.assertEqual(get_certificate_count(interface, 'ca'), 2) + self.assertEqual(get_certificate_count(interface, 'cert'), 3) + + # Check for running process + self.assertTrue(process_named_running('wpa_supplicant', cmdline=f'-i{interface}')) + + # Remove EAPoL configuration + for interface in self._interfaces: + self.cli_delete(self._base_path + [interface, 'eapol']) + + # Commit and check that process is no longer running + self.cli_commit() + self.assertFalse(process_named_running('wpa_supplicant')) + + for name in ca_certs: + self.cli_delete(['pki', 'ca', name]) + self.cli_delete(['pki', 'certificate', cert_name]) diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index 4843a40da..3d12364f7 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -15,7 +15,6 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import re import unittest from glob import glob @@ -28,86 +27,11 @@ from netifaces import ifaddresses from base_interfaces_test import BasicInterfaceTest from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section -from vyos.pki import CERT_BEGIN from vyos.utils.process import cmd -from vyos.utils.process import process_named_running from vyos.utils.process import popen from vyos.utils.file import read_file from vyos.utils.network import is_ipv6_link_local -server_ca_root_cert_data = """ -MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw -HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa -Fw0zMjAyMTUxOTQxMjBaMB4xHDAaBgNVBAMME1Z5T1Mgc2VydmVyIHJvb3QgQ0Ew -WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ0y24GzKQf4aM2Ir12tI9yITOIzAUj -ZXyJeCmYI6uAnyAMqc4Q4NKyfq3nBi4XP87cs1jlC1P2BZ8MsjL5MdGWozIwMDAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRwC/YaieMEnjhYa7K3Flw/o0SFuzAK -BggqhkjOPQQDAgNJADBGAiEAh3qEj8vScsjAdBy5shXzXDVVOKWCPTdGrPKnu8UW -a2cCIQDlDgkzWmn5ujc5ATKz1fj+Se/aeqwh4QyoWCVTFLIxhQ== -""" - -server_ca_intermediate_cert_data = """ -MIIBmTCCAT+gAwIBAgIUNzrtHzLmi3QpPK57tUgCnJZhXXQwCgYIKoZIzj0EAwIw -HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa -Fw0zMjAyMTUxOTQxMjFaMCYxJDAiBgNVBAMMG1Z5T1Mgc2VydmVyIGludGVybWVk -aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEl2nJ1CzoqPV6hWII2m -eGN/uieU6wDMECTk/LgG8CCCSYb488dibUiFN/1UFsmoLIdIhkx/6MUCYh62m8U2 -WNujUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMV3YwH88I5gFsFUibbQ -kMR0ECPsMB8GA1UdIwQYMBaAFHAL9hqJ4wSeOFhrsrcWXD+jRIW7MAoGCCqGSM49 -BAMCA0gAMEUCIQC/ahujD9dp5pMMCd3SZddqGC9cXtOwMN0JR3e5CxP13AIgIMQm -jMYrinFoInxmX64HfshYqnUY8608nK9D2BNPOHo= -""" - -client_ca_root_cert_data = """ -MIIBcDCCARagAwIBAgIUZmoW2xVdwkZSvglnkCq0AHKa6zIwCgYIKoZIzj0EAwIw -HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjFa -Fw0zMjAyMTUxOTQxMjFaMB4xHDAaBgNVBAMME1Z5T1MgY2xpZW50IHJvb3QgQ0Ew -WTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATUpKXzQk2NOVKDN4VULk2yw4mOKPvn -mg947+VY7lbpfOfAUD0QRg95qZWCw899eKnXp/U4TkAVrmEKhUb6OJTFozIwMDAP -BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTXu6xGWUl25X3sBtrhm3BJSICIATAK -BggqhkjOPQQDAgNIADBFAiEAnTzEwuTI9bz2Oae3LZbjP6f/f50KFJtjLZFDbQz7 -DpYCIDNRHV8zBUibC+zg5PqMpQBKd/oPfNU76nEv6xkp/ijO -""" - -client_ca_intermediate_cert_data = """ -MIIBmDCCAT+gAwIBAgIUJEMdotgqA7wU4XXJvEzDulUAGqgwCgYIKoZIzj0EAwIw -HjEcMBoGA1UEAwwTVnlPUyBjbGllbnQgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjJa -Fw0zMjAyMTUxOTQxMjJaMCYxJDAiBgNVBAMMG1Z5T1MgY2xpZW50IGludGVybWVk -aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGyIVIi217s9j3O+WQ2b -6R65/Z0ZjQpELxPjBRc0CA0GFCo+pI5EvwI+jNFArvTAJ5+ZdEWUJ1DQhBKDDQdI -avCjUzBRMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOUS8oNJjChB1Rb9Blcl -ETvziHJ9MB8GA1UdIwQYMBaAFNe7rEZZSXblfewG2uGbcElIgIgBMAoGCCqGSM49 -BAMCA0cAMEQCIArhaxWgRsAUbEeNHD/ULtstLHxw/P97qPUSROLQld53AiBjgiiz -9pDfISmpekZYz6bIDWRIR0cXUToZEMFNzNMrQg== -""" - -client_cert_data = """ -MIIBmTCCAUCgAwIBAgIUV5T77XdE/tV82Tk4Vzhp5BIFFm0wCgYIKoZIzj0EAwIw -JjEkMCIGA1UEAwwbVnlPUyBjbGllbnQgaW50ZXJtZWRpYXRlIENBMB4XDTIyMDIx -NzE5NDEyMloXDTMyMDIxNTE5NDEyMlowIjEgMB4GA1UEAwwXVnlPUyBjbGllbnQg -Y2VydGlmaWNhdGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARuyynqfc/qJj5e -KJ03oOH8X4Z8spDeAPO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAh -CIhytmJao1AwTjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTIFKrxZ+PqOhYSUqnl -TGCUmM7wTjAfBgNVHSMEGDAWgBTlEvKDSYwoQdUW/QZXJRE784hyfTAKBggqhkjO -PQQDAgNHADBEAiAvO8/jvz05xqmP3OXD53XhfxDLMIxzN4KPoCkFqvjlhQIgIHq2 -/geVx3rAOtSps56q/jiDouN/aw01TdpmGKVAa9U= -""" - -client_key_data = """ -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxaxAQsJwjoOCByQE -+qSYKtKtJzbdbOnTsKNSrfgkFH6hRANCAARuyynqfc/qJj5eKJ03oOH8X4Z8spDe -APO9WYckMM0ldPj+9kU607szFzPwjaPWzPdgyIWz3hcN8yAhCIhytmJa -""" - -def get_wpa_supplicant_value(interface, key): - tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') - tmp = re.findall(r'\n?{}=(.*)'.format(key), tmp) - return tmp[0] - -def get_certificate_count(interface, cert_type): - tmp = read_file(f'/run/wpa_supplicant/{interface}_{cert_type}.pem') - return tmp.count(CERT_BEGIN) - class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -237,72 +161,6 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_set(self._base_path + [interface, 'speed', 'auto']) self.cli_commit() - def test_eapol_support(self): - ca_certs = { - 'eapol-server-ca-root': server_ca_root_cert_data, - 'eapol-server-ca-intermediate': server_ca_intermediate_cert_data, - 'eapol-client-ca-root': client_ca_root_cert_data, - 'eapol-client-ca-intermediate': client_ca_intermediate_cert_data, - } - cert_name = 'eapol-client' - - for name, data in ca_certs.items(): - self.cli_set(['pki', 'ca', name, 'certificate', data.replace('\n','')]) - - self.cli_set(['pki', 'certificate', cert_name, 'certificate', client_cert_data.replace('\n','')]) - self.cli_set(['pki', 'certificate', cert_name, 'private', 'key', client_key_data.replace('\n','')]) - - for interface in self._interfaces: - # Enable EAPoL - self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-server-ca-intermediate']) - self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate']) - self.cli_set(self._base_path + [interface, 'eapol', 'certificate', cert_name]) - - self.cli_commit() - - # Test multiple CA chains - self.assertEqual(get_certificate_count(interface, 'ca'), 4) - - for interface in self._interfaces: - self.cli_delete(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol-client-ca-intermediate']) - - self.cli_commit() - - # Check for running process - self.assertTrue(process_named_running('wpa_supplicant')) - - # Validate interface config - for interface in self._interfaces: - tmp = get_wpa_supplicant_value(interface, 'key_mgmt') - self.assertEqual('IEEE8021X', tmp) - - tmp = get_wpa_supplicant_value(interface, 'eap') - self.assertEqual('TLS', tmp) - - tmp = get_wpa_supplicant_value(interface, 'eapol_flags') - self.assertEqual('0', tmp) - - tmp = get_wpa_supplicant_value(interface, 'ca_cert') - self.assertEqual(f'"/run/wpa_supplicant/{interface}_ca.pem"', tmp) - - tmp = get_wpa_supplicant_value(interface, 'client_cert') - self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.pem"', tmp) - - tmp = get_wpa_supplicant_value(interface, 'private_key') - self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.key"', tmp) - - mac = read_file(f'/sys/class/net/{interface}/address') - tmp = get_wpa_supplicant_value(interface, 'identity') - self.assertEqual(f'"{mac}"', tmp) - - # Check certificate files have the full chain - self.assertEqual(get_certificate_count(interface, 'ca'), 2) - self.assertEqual(get_certificate_count(interface, 'cert'), 3) - - for name in ca_certs: - self.cli_delete(['pki', 'ca', name]) - self.cli_delete(['pki', 'certificate', cert_name]) - def test_ethtool_ring_buffer(self): for interface in self._interfaces: # We do not use vyos.ethtool here to not have any chance diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 5e5d5fba1..bbbfb0385 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -25,6 +25,7 @@ from vyos.configdict import is_source_interface from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_dhcpv6 +from vyos.configverify import verify_eapol from vyos.configverify import verify_mirror_redirect from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config @@ -73,7 +74,7 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'bonding'] - ifname, bond = get_interface_dict(conf, base) + ifname, bond = get_interface_dict(conf, base, with_pki=True) # To make our own life easier transfor the list of member interfaces # into a dictionary - we will use this to add additional information @@ -196,6 +197,7 @@ def verify(bond): verify_dhcpv6(bond) verify_vrf(bond) verify_mirror_redirect(bond) + verify_eapol(bond) # use common function to verify VLAN configuration verify_vlan_config(bond) diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py index afc48ead8..34ce7bc47 100755 --- a/src/conf_mode/interfaces_ethernet.py +++ b/src/conf_mode/interfaces_ethernet.py @@ -31,32 +31,20 @@ from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.configverify import verify_bond_bridge_member -from vyos.configverify import verify_pki_certificate -from vyos.configverify import verify_pki_ca_certificate +from vyos.configverify import verify_eapol from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf from vyos.ifconfig import BondIf -from vyos.pki import find_chain -from vyos.pki import encode_certificate -from vyos.pki import load_certificate -from vyos.pki import wrap_private_key -from vyos.template import render from vyos.template import render_to_string -from vyos.utils.process import call from vyos.utils.dict import dict_search from vyos.utils.dict import dict_to_paths_values from vyos.utils.dict import dict_set from vyos.utils.dict import dict_delete -from vyos.utils.file import write_file from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() -# XXX: wpa_supplicant works on the source interface -cfg_dir = '/run/wpa_supplicant' -wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' - def update_bond_options(conf: Config, eth_conf: dict) -> list: """ Return list of blocked options if interface is a bond member @@ -277,23 +265,6 @@ def verify_allowedbond_changes(ethernet: dict): f' on interface "{ethernet["ifname"]}".' \ f' Interface is a bond member') -def verify_eapol(ethernet: dict): - """ - Common helper function used by interface implementations to perform - recurring validation of EAPoL configuration. - """ - if 'eapol' not in ethernet: - return - - if 'certificate' not in ethernet['eapol']: - raise ConfigError('Certificate must be specified when using EAPoL!') - - verify_pki_certificate(ethernet, ethernet['eapol']['certificate'], no_password_protected=True) - - if 'ca_certificate' in ethernet['eapol']: - for ca_cert in ethernet['eapol']['ca_certificate']: - verify_pki_ca_certificate(ethernet, ca_cert) - def verify(ethernet): if 'deleted' in ethernet: return None @@ -346,51 +317,10 @@ def verify_ethernet(ethernet): verify_vlan_config(ethernet) return None - def generate(ethernet): - # render real configuration file once - wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet) - if 'deleted' in ethernet: - # delete configuration on interface removal - if os.path.isfile(wpa_supplicant_conf): - os.unlink(wpa_supplicant_conf) return None - if 'eapol' in ethernet: - ifname = ethernet['ifname'] - - render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet) - - cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem') - cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key') - - cert_name = ethernet['eapol']['certificate'] - pki_cert = ethernet['pki']['certificate'][cert_name] - - loaded_pki_cert = load_certificate(pki_cert['certificate']) - loaded_ca_certs = {load_certificate(c['certificate']) - for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {} - - cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs) - - write_file(cert_file_path, - '\n'.join(encode_certificate(c) for c in cert_full_chain)) - write_file(cert_key_path, wrap_private_key(pki_cert['private']['key'])) - - if 'ca_certificate' in ethernet['eapol']: - ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem') - ca_chains = [] - - for ca_cert_name in ethernet['eapol']['ca_certificate']: - pki_ca_cert = ethernet['pki']['ca'][ca_cert_name] - loaded_ca_cert = load_certificate(pki_ca_cert['certificate']) - ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs) - ca_chains.append( - '\n'.join(encode_certificate(c) for c in ca_full_chain)) - - write_file(ca_cert_file_path, '\n'.join(ca_chains)) - ethernet['frr_zebra_config'] = '' if 'deleted' not in ethernet: ethernet['frr_zebra_config'] = render_to_string('frr/evpn.mh.frr.j2', ethernet) @@ -399,8 +329,6 @@ def generate(ethernet): def apply(ethernet): ifname = ethernet['ifname'] - # take care about EAPoL supplicant daemon - eapol_action='stop' e = EthernetIf(ifname) if 'deleted' in ethernet: @@ -408,10 +336,6 @@ def apply(ethernet): e.remove() else: e.update(ethernet) - if 'eapol' in ethernet: - eapol_action='reload-or-restart' - - call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}') zebra_daemon = 'zebra' # Save original configuration prior to starting any commit actions diff --git a/src/op_mode/vpn_ike_sa.py b/src/op_mode/vpn_ike_sa.py index 5e2aaae6b..9385bcd0c 100755 --- a/src/op_mode/vpn_ike_sa.py +++ b/src/op_mode/vpn_ike_sa.py @@ -38,6 +38,8 @@ def ike_sa(peer, nat): peers = [] for conn in sas: for name, sa in conn.items(): + if peer and s(sa['remote-host']) != peer: + continue if name.startswith('peer_') and name in peers: continue if nat and 'nat-local' not in sa: |