summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/macsec/wpa_supplicant.conf.tmpl65
-rw-r--r--debian/control1
-rw-r--r--interface-definitions/include/source-interface-ethernet.xml.i12
-rw-r--r--interface-definitions/interfaces-macsec.xml.in91
-rw-r--r--interface-definitions/interfaces-pseudo-ethernet.xml.in13
-rw-r--r--op-mode-definitions/generate-macsec-key.xml26
-rw-r--r--op-mode-definitions/show-interfaces-macsec.xml29
-rw-r--r--python/vyos/ifconfig/__init__.py1
-rw-r--r--python/vyos/ifconfig/interface.py4
-rw-r--r--python/vyos/ifconfig/macsec.py73
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py216
-rw-r--r--src/systemd/wpa_supplicant-macsec@.service17
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