Skip to content

Commit

Permalink
Merge pull request #4069 from c-po/eapol-bond
Browse files Browse the repository at this point in the history
bond: T6709: add EAPoL support
  • Loading branch information
c-po authored Sep 16, 2024
2 parents 11164c8 + 8eeb1bd commit 27e2016
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 225 deletions.
1 change: 1 addition & 0 deletions interface-definitions/interfaces_bonding.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
17 changes: 17 additions & 0 deletions python/vyos/configverify.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
72 changes: 68 additions & 4 deletions python/vyos/ifconfig/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
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
Expand All @@ -41,9 +47,8 @@
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
Expand All @@ -52,7 +57,6 @@
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
Expand Down Expand Up @@ -377,6 +381,9 @@ def remove(self):
>>> 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
Expand Down Expand Up @@ -1522,6 +1529,61 @@ def set_per_client_thread(self, enable):
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
Expand Down Expand Up @@ -1609,7 +1671,6 @@ def update(self, config):
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', ''))

Expand Down Expand Up @@ -1752,6 +1813,9 @@ def update(self, config):
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
Expand Down
162 changes: 161 additions & 1 deletion smoketest/scripts/cli/base_interfaces_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +24,7 @@
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
Expand All @@ -40,6 +43,79 @@
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.
Expand All @@ -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
Expand Down Expand Up @@ -92,6 +168,7 @@ def setUpClass(cls):
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')
Expand Down Expand Up @@ -1158,3 +1235,86 @@ def test_dhcpv6pd_manual_sla_id(self):
# 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])
Loading

0 comments on commit 27e2016

Please sign in to comment.