diff options
-rw-r--r-- | data/templates/macsec/wpa_supplicant.conf.tmpl | 65 | ||||
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | interface-definitions/include/source-interface-ethernet.xml.i | 12 | ||||
-rw-r--r-- | interface-definitions/interfaces-macsec.xml.in | 91 | ||||
-rw-r--r-- | interface-definitions/interfaces-pseudo-ethernet.xml.in | 13 | ||||
-rw-r--r-- | op-mode-definitions/generate-macsec-key.xml | 26 | ||||
-rw-r--r-- | op-mode-definitions/show-interfaces-macsec.xml | 29 | ||||
-rw-r--r-- | python/vyos/ifconfig/__init__.py | 1 | ||||
-rw-r--r-- | python/vyos/ifconfig/interface.py | 4 | ||||
-rw-r--r-- | python/vyos/ifconfig/macsec.py | 73 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-macsec.py | 216 | ||||
-rw-r--r-- | src/systemd/wpa_supplicant-macsec@.service | 17 |
12 files changed, 534 insertions, 14 deletions
diff --git a/data/templates/macsec/wpa_supplicant.conf.tmpl b/data/templates/macsec/wpa_supplicant.conf.tmpl new file mode 100644 index 000000000..eee215418 --- /dev/null +++ b/data/templates/macsec/wpa_supplicant.conf.tmpl @@ -0,0 +1,65 @@ +# autogenerated by interfaces-macsec.py + +# see full documentation: +# https://w1.fi/cgit/hostap/plain/wpa_supplicant/wpa_supplicant.conf + +# For UNIX domain sockets (default on Linux and BSD): This is a directory that +# will be created for UNIX domain sockets for listening to requests from +# external programs (CLI/GUI, etc.) for status information and configuration. +# The socket file will be named based on the interface name, so multiple +# wpa_supplicant processes can be run at the same time if more than one +# interface is used. +# /var/run/wpa_supplicant is the recommended directory for sockets and by +# default, wpa_cli will use it when trying to connect with wpa_supplicant. +ctrl_interface=/run/wpa_supplicant + +# Note: When using MACsec, eapol_version shall be set to 3, which is +# defined in IEEE Std 802.1X-2010. +eapol_version=3 + +# No need to scan for access points in MACsec mode +ap_scan=0 + +# EAP fast re-authentication +fast_reauth=1 + +network={ + key_mgmt=NONE + + # Note: When using wired authentication (including MACsec drivers), + # eapol_flags must be set to 0 for the authentication to be completed + # successfully. + eapol_flags=0 + + # macsec_policy: IEEE 802.1X/MACsec options + # This determines how sessions are secured with MACsec (only for MACsec + # drivers). + # 0: MACsec not in use (default) + # 1: MACsec enabled - Should secure, accept key server's advice to + # determine whether to use a secure session or not. + macsec_policy=1 + + # macsec_integ_only: IEEE 802.1X/MACsec transmit mode + # This setting applies only when MACsec is in use, i.e., + # - macsec_policy is enabled + # - the key server has decided to enable MACsec + # 0: Encrypt traffic (default) + # 1: Integrity only + macsec_integ_only={{ '0' if security_encrypt else '1' }} + + # mka_cak, mka_ckn, and mka_priority: IEEE 802.1X/MACsec pre-shared key mode + # This allows to configure MACsec with a pre-shared key using a (CAK,CKN) pair. + # In this mode, instances of wpa_supplicant can act as MACsec peers. The peer + # with lower priority will become the key server and start distributing SAKs. + # mka_cak (CAK = Secure Connectivity Association Key) takes a 16-byte (128-bit) + # hex-string (32 hex-digits) or a 32-byte (256-bit) hex-string (64 hex-digits) + # mka_ckn (CKN = CAK Name) takes a 1..32-bytes (8..256 bit) hex-string + # (2..64 hex-digits) + mka_cak={{ security_mka_cak }} + mka_ckn={{ security_mka_ckn }} + + # mka_priority (Priority of MKA Actor) is in 0..255 range with 255 being + # default priority + mka_priority={{ security_mka_priority }} +} + diff --git a/debian/control b/debian/control index eec2c087e..a30b80b7a 100644 --- a/debian/control +++ b/debian/control @@ -33,6 +33,7 @@ Depends: python3, python3-netaddr, python3-zmq, python3-jmespath, + bsdmainutils, cron, systemd, easy-rsa, diff --git a/interface-definitions/include/source-interface-ethernet.xml.i b/interface-definitions/include/source-interface-ethernet.xml.i new file mode 100644 index 000000000..ad90bc4ac --- /dev/null +++ b/interface-definitions/include/source-interface-ethernet.xml.i @@ -0,0 +1,12 @@ +<leafNode name="source-interface"> + <properties> + <help>Physical interface the traffic will go through</help> + <valueHelp> + <format>interface</format> + <description>Physical interface used for traffic forwarding</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py -t ethernet</script> + </completionHelp> + </properties> +</leafNode> diff --git a/interface-definitions/interfaces-macsec.xml.in b/interface-definitions/interfaces-macsec.xml.in new file mode 100644 index 000000000..af3971595 --- /dev/null +++ b/interface-definitions/interfaces-macsec.xml.in @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="interfaces"> + <children> + <tagNode name="macsec" owner="${vyos_conf_scripts_dir}/interfaces-macsec.py"> + <properties> + <help>MACsec Interface (802.1ae)</help> + <priority>319</priority> + <constraint> + <regex>^macsec[0-9]+$</regex> + </constraint> + <constraintErrorMessage>MACsec interface must be named macsecN</constraintErrorMessage> + <valueHelp> + <format>macsecN</format> + <description>MACsec interface name</description> + </valueHelp> + </properties> + <children> + #include <include/address-ipv4-ipv6.xml.i> + <node name="security"> + <properties> + <help>Security/Encryption Settings</help> + </properties> + <children> + <leafNode name="cipher"> + <properties> + <help>Cipher suite used</help> + <completionHelp> + <list>gcm-aes-128</list> + </completionHelp> + <valueHelp> + <format>gcm-aes-128</format> + <description>Galois/Counter Mode of AES cipher with 128-bit key (default)</description> + </valueHelp> + <constraint> + <regex>(gcm-aes-128)</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="encrypt"> + <properties> + <help>Enable optional MACsec encryption</help> + <valueless/> + </properties> + </leafNode> + <node name="mka"> + <properties> + <help>MACsec Key Agreement protocol (MKA)</help> + </properties> + <children> + <leafNode name="cak"> + <properties> + <help>Secure Connectivity Association Key</help> + <constraint> + <regex>^[A-Fa-f0-9]{32}$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="ckn"> + <properties> + <help>Secure Connectivity Association Key Name</help> + <constraint> + <regex>^[A-Fa-f0-9]{64}$</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="priority"> + <properties> + <help>Priority of MACsec Key Agreement protocol (MKA) actor (default: 255)</help> + <valueHelp> + <format>0-255</format> + <description>MACsec Key Agreement protocol (MKA) priority</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-255" /> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + #include <include/interface-description.xml.i> + #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> + #include <include/source-interface-ethernet.xml.i> + </children> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/interfaces-pseudo-ethernet.xml.in b/interface-definitions/interfaces-pseudo-ethernet.xml.in index 61fd6c9fd..d5f9ca661 100644 --- a/interface-definitions/interfaces-pseudo-ethernet.xml.in +++ b/interface-definitions/interfaces-pseudo-ethernet.xml.in @@ -41,18 +41,7 @@ #include <include/ipv6-dup-addr-detect-transmits.xml.i> </children> </node> - <leafNode name="source-interface"> - <properties> - <help>Physical Interface used for this device</help> - <valueHelp> - <format>interface</format> - <description>Physical interface used for this pseudo device</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py -t ethernet</script> - </completionHelp> - </properties> - </leafNode> + #include <include/source-interface-ethernet.xml.i> #include <include/interface-mac.xml.i> <leafNode name="mode"> <properties> diff --git a/op-mode-definitions/generate-macsec-key.xml b/op-mode-definitions/generate-macsec-key.xml new file mode 100644 index 000000000..40d2b9061 --- /dev/null +++ b/op-mode-definitions/generate-macsec-key.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="macsec"> + <properties> + <help>Generate MACsec Key</help> + </properties> + <children> + <node name="mka-cak"> + <properties> + <help>Generate MACsec connectivity association key (CAK)</help> + </properties> + <command>/usr/bin/hexdump -n 16 -e '4/4 "%08x" 1 "\n"' /dev/random</command> + </node> + <node name="mka-ckn"> + <properties> + <help>Generate MACsec connectivity association name (CKN)</help> + </properties> + <command>/usr/bin/hexdump -n 32 -e '8/4 "%08x" 1 "\n"' /dev/random</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-interfaces-macsec.xml b/op-mode-definitions/show-interfaces-macsec.xml new file mode 100644 index 000000000..6aeab66af --- /dev/null +++ b/op-mode-definitions/show-interfaces-macsec.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <node name="macsec"> + <properties> + <help>Show MACsec interface information</help> + <completionHelp> + <path>interfaces macsec</path> + </completionHelp> + </properties> + <command>/usr/sbin/ip macsec show</command> + </node> + <tagNode name="macsec"> + <properties> + <help>Show specified MACsec interface information</help> + <completionHelp> + <path>interfaces macsec</path> + </completionHelp> + </properties> + <command>/usr/sbin/ip macsec show $4</command> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/ifconfig/__init__.py b/python/vyos/ifconfig/__init__.py index 4d98901b7..1757adf26 100644 --- a/python/vyos/ifconfig/__init__.py +++ b/python/vyos/ifconfig/__init__.py @@ -42,3 +42,4 @@ from vyos.ifconfig.tunnel import SitIf from vyos.ifconfig.tunnel import Sit6RDIf from vyos.ifconfig.wireless import WiFiIf from vyos.ifconfig.l2tpv3 import L2TPv3If +from vyos.ifconfig.macsec import MACsecIf diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 61f2c6482..07efc6d97 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -51,7 +51,7 @@ class Interface(Control): # WireGuard to modify their display behaviour OperationalClass = Operational - options = ['debug', 'create',] + options = ['debug', 'create'] required = [] default = { 'type': '', @@ -265,7 +265,7 @@ class Interface(Control): # NOTE (Improvement): # after interface removal no other commands should be allowed # to be called and instead should raise an Exception: - cmd = 'ip link del dev {}'.format(self.config['ifname']) + cmd = 'ip link del dev {ifname}'.format(**self.config) return self._cmd(cmd) def get_mtu(self): diff --git a/python/vyos/ifconfig/macsec.py b/python/vyos/ifconfig/macsec.py new file mode 100644 index 000000000..ea8c9807e --- /dev/null +++ b/python/vyos/ifconfig/macsec.py @@ -0,0 +1,73 @@ +# Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.ifconfig.interface import Interface + +@Interface.register +class MACsecIf(Interface): + """ + MACsec is an IEEE standard (IEEE 802.1AE) for MAC security, introduced in + 2006. It defines a way to establish a protocol independent connection + between two hosts with data confidentiality, authenticity and/or integrity, + using GCM-AES-128. MACsec operates on the Ethernet layer and as such is a + layer 2 protocol, which means it's designed to secure traffic within a + layer 2 network, including DHCP or ARP requests. It does not compete with + other security solutions such as IPsec (layer 3) or TLS (layer 4), as all + those solutions are used for their own specific use cases. + """ + + default = { + 'type': 'macsec', + 'security_cipher': '', + 'source_interface': '' + } + definition = { + **Interface.definition, + **{ + 'section': 'macsec', + 'prefixes': ['macsec', ], + }, + } + options = Interface.options + \ + ['security_cipher', 'source_interface'] + + def _create(self): + """ + Create MACsec interface in OS kernel. Interface is administrative + down by default. + """ + # create tunnel interface + cmd = 'ip link add link {source_interface} {ifname} type {type}' + cmd += ' cipher {security_cipher}' + self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly + self.set_admin_state('down') + + @staticmethod + def get_config(): + """ + MACsec interfaces require a configuration when they are added using + iproute2. This static method will provide the configuration dictionary + used by this class. + + Example: + >> dict = MACsecIf().get_config() + """ + config = { + 'security_cipher': '', + 'source_interface': '', + } + return config diff --git a/src/conf_mode/interfaces-macsec.py b/src/conf_mode/interfaces-macsec.py new file mode 100755 index 000000000..ed88e877d --- /dev/null +++ b/src/conf_mode/interfaces-macsec.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2020 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 os + +from copy import deepcopy +from sys import exit +from netifaces import interfaces + +from vyos.config import Config +from vyos.configdict import list_diff +from vyos.ifconfig import MACsecIf, Interface +from vyos.template import render +from vyos.util import call +from vyos.validate import is_member +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'address_remove': [], + 'deleted': False, + 'description': '', + 'disable': False, + 'security_cipher': '', + 'security_encrypt': False, + 'security_mka_cak': '', + 'security_mka_ckn': '', + 'security_mka_priority': '255', + 'intf': '', + 'source_interface': '', + 'is_bridge_member': False, + 'vrf': '' +} + +def get_config(): + macsec = deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + if 'VYOS_TAGNODE_VALUE' not in os.environ: + raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') + + macsec['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + base_path = ['interfaces', 'macsec', macsec['intf']] + + # check if we are a member of any bridge + macsec['is_bridge_member'] = is_member(conf, macsec['intf'], 'bridge') + + # Check if interface has been removed + if not conf.exists(base_path): + macsec['deleted'] = True + return macsec + + # set new configuration level + conf.set_level(base_path) + + # retrieve configured interface addresses + if conf.exists(['address']): + macsec['address'] = conf.return_values(['address']) + + # retrieve interface description + if conf.exists(['description']): + macsec['description'] = conf.return_value(['description']) + + # Disable this interface + if conf.exists(['disable']): + macsec['disable'] = True + + # retrieve interface cipher + if conf.exists(['security', 'cipher']): + macsec['security_cipher'] = conf.return_value(['security', 'cipher']) + + # Enable optional MACsec encryption + if conf.exists(['security', 'encrypt']): + macsec['security_encrypt'] = True + + # Secure Connectivity Association Key + if conf.exists(['security', 'mka', 'cak']): + macsec['security_mka_cak'] = conf.return_value(['security', 'mka', 'cak']) + + # Secure Connectivity Association Name + if conf.exists(['security', 'mka', 'ckn']): + macsec['security_mka_ckn'] = conf.return_value(['security', 'mka', 'ckn']) + + # MACsec Key Agreement protocol (MKA) actor priority + if conf.exists(['security', 'mka', 'priority']): + macsec['security_mka_priority'] = conf.return_value(['security', 'mka', 'priority']) + + # Physical interface + if conf.exists(['source-interface']): + macsec['source_interface'] = conf.return_value(['source-interface']) + + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the interface + eff_addr = conf.return_effective_values(['address']) + act_addr = conf.return_values(['address']) + macsec['address_remove'] = list_diff(eff_addr, act_addr) + + # retrieve VRF instance + if conf.exists(['vrf']): + macsec['vrf'] = conf.return_value(['vrf']) + + return macsec + +def verify(macsec): + if macsec['deleted']: + if macsec['is_bridge_member']: + raise ConfigError( + f'Interface "{intf}" cannot be deleted as it is a ' + f'member of bridge "{is_bridge_member}"!'.format(**macsec)) + + return None + + if not macsec['source_interface']: + raise ConfigError( + 'Physical source interface must be set for MACsec "{intf}"'.format(**macsec)) + + if not macsec['security_cipher']: + raise ConfigError( + 'Cipher suite must be set for MACsec "{intf}"'.format(**macsec)) + + if macsec['security_encrypt']: + if not (macsec['security_mka_cak'] and macsec['security_mka_ckn']): + raise ConfigError('MACsec security keys mandartory when encryption is enabled') + + if macsec['vrf']: + if macsec['vrf'] not in interfaces(): + raise ConfigError('VRF "{vrf}" does not exist'.format(**macsec)) + + if macsec['is_bridge_member']: + raise ConfigError( + 'Interface "{intf}" cannot be member of VRF "{vrf}" and ' + 'bridge "{is_bridge_member}" at the same time!'.format(**macsec)) + + if macsec['is_bridge_member'] and macsec['address']: + raise ConfigError( + 'Cannot assign address to interface "{intf}" as it is' + 'a member of bridge "{is_bridge_member}"!'.format(**macsec)) + + return None + +def generate(macsec): + # XXX: wpa_supplicant works on the source interface not the resulting + # MACsec interface + wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf' + conf = wpa_suppl_conf.format(**macsec) + + render(conf, 'macsec/wpa_supplicant.conf.tmpl', macsec, permission=0o640) + return None + +def apply(macsec): + # Remove macsec interface + if macsec['deleted']: + call('systemctl stop wpa_supplicant-macsec@{intf}.service'.format(**macsec)) + MACsecIf(macsec['intf']).remove() + + else: + # MACsec interfaces require a configuration when they are added using + # iproute2. This static method will provide the configuration + # dictionary used by this class. + conf = deepcopy(MACsecIf.get_config()) + + # Assign MACsec instance configuration parameters to config dict + conf['source_interface'] = macsec['source_interface'] + conf['security_cipher'] = macsec['security_cipher'] + + # It is safe to "re-create" the interface always, there is a sanity check + # that the interface will only be create if its non existent + i = MACsecIf(macsec['intf'], **conf) + + # update interface description used e.g. within SNMP + i.set_alias(macsec['description']) + + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in macsec['address_remove']: + i.del_addr(addr) + for addr in macsec['address']: + i.add_addr(addr) + + # assign/remove VRF (ONLY when not a member of a bridge, + # otherwise 'nomaster' removes it from it) + if not macsec['is_bridge_member']: + i.set_vrf(macsec['vrf']) + + # Interface is administratively down by default, enable if desired + if not macsec['disable']: + i.set_admin_state('up') + + call('systemctl restart wpa_supplicant-macsec@{source_interface}.service'.format(**macsec)) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/systemd/wpa_supplicant-macsec@.service b/src/systemd/wpa_supplicant-macsec@.service new file mode 100644 index 000000000..9fad6b773 --- /dev/null +++ b/src/systemd/wpa_supplicant-macsec@.service @@ -0,0 +1,17 @@ +[Unit]
+Description=WPA supplicant daemon (macsec-specific version)
+Requires=sys-subsystem-net-devices-%i.device
+ConditionPathExists=/run/wpa_supplicant/%I.conf
+After=vyos-router.service
+RequiresMountsFor=/run
+
+# NetworkManager users will probably want the dbus version instead.
+
+[Service]
+Type=simple
+WorkingDirectory=/run/wpa_supplicant
+PIDFile=/run/wpa_supplicant/%I.pid
+ExecStart=/sbin/wpa_supplicant -c /run/wpa_supplicant/%I.conf -Dmacsec_linux -i%I
+
+[Install]
+WantedBy=multi-user.target
|