Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F117520856
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
225 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 5ca369f66..8325355e8 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -1,429 +1,435 @@
# Copyright 2019 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/>.
"""
A library for retrieving value dicts from VyOS configs in a declarative fashion.
"""
from enum import Enum
from copy import deepcopy
from vyos import ConfigError
from vyos.ifconfig import Interface
from vyos.validate import is_member
from vyos.util import ifname_from_config
def retrieve_config(path_hash, base_path, config):
"""
Retrieves a VyOS config as a dict according to a declarative description
The description dict, passed in the first argument, must follow this format:
``field_name : <path, type, [inner_options_dict]>``.
Supported types are: ``str`` (for normal nodes),
``list`` (returns a list of strings, for multi nodes),
``bool`` (returns True if valueless node exists),
``dict`` (for tag nodes, returns a dict indexed by node names,
according to description in the third item of the tuple).
Args:
path_hash (dict): Declarative description of the config to retrieve
base_path (list): A base path to prepend to all option paths
config (vyos.config.Config): A VyOS config object
Returns:
dict: config dict
"""
config_hash = {}
for k in path_hash:
if type(path_hash[k]) != tuple:
raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k])))
if len(path_hash[k]) < 2:
raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k))
path = path_hash[k][0]
if type(path) != list:
raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path)))
typ = path_hash[k][1]
if type(typ) != type:
raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ)))
path = base_path + path
path_str = " ".join(path)
if typ == str:
config_hash[k] = config.return_value(path_str)
elif typ == list:
config_hash[k] = config.return_values(path_str)
elif typ == bool:
config_hash[k] = config.exists(path_str)
elif typ == dict:
try:
inner_hash = path_hash[k][2]
except IndexError:
raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k))
config_hash[k] = {}
nodes = config.list_nodes(path_str)
for node in nodes:
config_hash[k][node] = retrieve_config(inner_hash, path + [node], config)
return config_hash
def list_diff(first, second):
"""
Diff two dictionaries and return only unique items
"""
second = set(second)
return [item for item in first if item not in second]
def get_ethertype(ethertype_val):
if ethertype_val == '0x88A8':
return '802.1ad'
elif ethertype_val == '0x8100':
return '802.1q'
else:
raise ConfigError('invalid ethertype "{}"'.format(ethertype_val))
vlan_default = {
'address': [],
'address_remove': [],
'description': '',
'dhcp_client_id': '',
'dhcp_hostname': '',
'dhcp_vendor_class_id': '',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
'disable': False,
'disable_link_detect': 1,
'egress_qos': '',
'egress_qos_changed': False,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'ingress_qos': '',
'ingress_qos_changed': False,
'is_bridge_member': False,
'mac': '',
'mtu': 1500,
'vif_c': {},
'vif_c_remove': [],
'vrf': ''
}
# see: https://docs.python.org/3/library/enum.html#functional-api
disable = Enum('disable','none was now both')
def disable_state(conf, check=[3,5,7]):
"""
return if and how a particual section of the configuration is has disable'd
using "disable" including if it was disabled by one of its parent.
check: a list of the level we should check, here 7,5 and 3
interfaces ethernet eth1 vif-s 1 vif-c 2 disable
interfaces ethernet eth1 vif 1 disable
interfaces ethernet eth1 disable
it returns an enum (none, was, now, both)
"""
# save where we are in the config
current_level = conf.get_level()
# logic to figure out if the interface (or one of it parent is disabled)
eff_disable = False
act_disable = False
levels = check[:]
working_level = current_level[:]
while levels:
position = len(working_level)
if not position:
break
if position not in levels:
working_level = working_level[:-1]
continue
levels.remove(position)
conf.set_level(working_level)
working_level = working_level[:-1]
eff_disable = eff_disable or conf.exists_effective('disable')
act_disable = act_disable or conf.exists('disable')
conf.set_level(current_level)
# how the disabling changed
if eff_disable and act_disable:
return disable.both
if eff_disable and not eff_disable:
return disable.was
if not eff_disable and act_disable:
return disable.now
return disable.none
def intf_to_dict(conf, default):
"""
Common used function which will extract VLAN related information from config
and represent the result as Python dictionary.
Function call's itself recursively if a vif-s/vif-c pair is detected.
"""
intf = deepcopy(default)
intf['intf'] = ifname_from_config(conf)
# retrieve interface description
if conf.exists('description'):
intf['description'] = conf.return_value('description')
# get DHCP client identifier
if conf.exists('dhcp-options client-id'):
intf['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
# DHCP client host name (overrides the system host name)
if conf.exists('dhcp-options host-name'):
intf['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
# DHCP client vendor identifier
if conf.exists('dhcp-options vendor-class-id'):
intf['dhcp_vendor_class_id'] = conf.return_value(
'dhcp-options vendor-class-id')
# DHCPv6 only acquire config parameters, no address
if conf.exists('dhcpv6-options parameters-only'):
intf['dhcpv6_prm_only'] = True
# DHCPv6 temporary IPv6 address
if conf.exists('dhcpv6-options temporary'):
intf['dhcpv6_temporary'] = True
# ignore link state changes
if conf.exists('disable-link-detect'):
intf['disable_link_detect'] = 2
# ARP filter configuration
if conf.exists('ip disable-arp-filter'):
intf['ip_disable_arp_filter'] = 0
# ARP enable accept
if conf.exists('ip enable-arp-accept'):
intf['ip_enable_arp_accept'] = 1
# ARP enable announce
if conf.exists('ip enable-arp-announce'):
intf['ip_enable_arp_announce'] = 1
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
intf['ip_enable_arp_ignore'] = 1
# Enable Proxy ARP
if conf.exists('ip enable-proxy-arp'):
intf['ip_proxy_arp'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
intf['ipv6_autoconf'] = 1
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
intf['ipv6_forwarding'] = 0
# check if interface is member of a bridge
intf['is_bridge_member'] = is_member(conf, intf['intf'], 'bridge')
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
intf['ipv6_dup_addr_detect'] = int(
conf.return_value('ipv6 dup-addr-detect-transmits'))
# Media Access Control (MAC) address
if conf.exists('mac'):
intf['mac'] = conf.return_value('mac')
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
intf['mtu'] = int(conf.return_value('mtu'))
# retrieve VRF instance
if conf.exists('vrf'):
intf['vrf'] = conf.return_value('vrf')
# egress QoS
if conf.exists('egress-qos'):
intf['egress_qos'] = conf.return_value('egress-qos')
# egress changes QoS require VLAN interface recreation
if conf.return_effective_value('egress-qos'):
if intf['egress_qos'] != conf.return_effective_value('egress-qos'):
intf['egress_qos_changed'] = True
# ingress QoS
if conf.exists('ingress-qos'):
intf['ingress_qos'] = conf.return_value('ingress-qos')
# ingress changes QoS require VLAN interface recreation
if conf.return_effective_value('ingress-qos'):
if intf['ingress_qos'] != conf.return_effective_value('ingress-qos'):
intf['ingress_qos_changed'] = True
# Get the interface addresses
intf['address'] = conf.return_values('address')
# addresses to remove - difference between effective and working config
intf['address_remove'] = list_diff(
conf.return_effective_values('address'),
intf['address']
)
# Get prefixes for IPv6 addressing based on MAC address (EUI-64)
intf['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# EUI64 to remove - difference between effective and working config
intf['ipv6_eui64_prefix_remove'] = list_diff(
conf.return_effective_values('ipv6 address eui64'),
intf['ipv6_eui64_prefix']
)
# Determine if the interface should be disabled
disabled = disable_state(conf)
if disabled == disable.both:
# was and is still disabled
intf['disable'] = True
elif disabled == disable.now:
# it is now disable but was not before
intf['disable'] = True
elif disabled == disable.was:
# it was disable but not anymore
intf['disable'] = False
else:
# normal change
intf['disable'] = False
# Remove the default link-local address if no-default-link-local is set,
# if member of a bridge or if disabled (it may not have a MAC if it's down)
if ( conf.exists('ipv6 address no-default-link-local')
or intf.get('is_bridge_member')
or intf['disable'] ):
intf['ipv6_eui64_prefix_remove'].append('fe80::/64')
else:
# add the link-local by default to make IPv6 work
intf['ipv6_eui64_prefix'].append('fe80::/64')
# If MAC has changed, remove and re-add all IPv6 EUI64 addresses
try:
interface = Interface(intf['intf'], create=False)
if intf['mac'] and intf['mac'] != interface.get_mac():
intf['ipv6_eui64_prefix_remove'] += intf['ipv6_eui64_prefix']
except Exception:
# If the interface does not exist, it could not have changed
pass
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if intf['ipv6_autoconf'] or 'dhcpv6' in intf['address']:
+ intf['ipv6_accept_ra'] = 2
+
return intf, disable
def add_to_dict(conf, disabled, ifdict, section, key):
"""
parse a section of vif/vif-s/vif-c and add them to the dict
follow the convention to:
* use the "key" for what to add
* use the "key" what what to remove
conf: is the Config() already at the level we need to parse
disabled: is a disable enum so we know how to handle to data
intf: if the interface dictionary
section: is the section name to parse (vif/vif-s/vif-c)
key: is the dict key to use (vif/vifs/vifc)
"""
if not conf.exists(section):
return ifdict
effect = conf.list_effective_nodes(section)
active = conf.list_nodes(section)
# the section to parse for vlan
sections = []
# determine which interfaces to add or remove based on disable state
if disabled == disable.both:
# was and is still disabled
ifdict[f'{key}_remove'] = []
elif disabled == disable.now:
# it is now disable but was not before
ifdict[f'{key}_remove'] = effect
elif disabled == disable.was:
# it was disable but not anymore
ifdict[f'{key}_remove'] = []
sections = active
else:
# normal change
# get interfaces (currently effective) - to determine which
# interface is no longer present and needs to be removed
ifdict[f'{key}_remove'] = list_diff(effect, active)
sections = active
current_level = conf.get_level()
# add each section, the key must already exists
for s in sections:
# set config level to vif interface
conf.set_level(current_level + [section, s])
# add the vlan config as a key (vlan id) - value (config) pair
ifdict[key][s] = vlan_to_dict(conf)
# re-set configuration level to leave things as found
conf.set_level(current_level)
return ifdict
def vlan_to_dict(conf, default=vlan_default):
vlan, disabled = intf_to_dict(conf, default)
# if this is a not within vif-s node, we are done
if conf.get_level()[-2] != 'vif-s':
return vlan
# ethertype is mandatory on vif-s nodes and only exists here!
# ethertype uses a default of 0x88A8
tmp = '0x88A8'
if conf.exists('ethertype'):
tmp = conf.return_value('ethertype')
vlan['ethertype'] = get_ethertype(tmp)
# check if there is a Q-in-Q vlan customer interface
# and call this function recursively
add_to_dict(conf, disable, vlan, 'vif-c', 'vif_c')
return vlan
diff --git a/python/vyos/ifconfig/dhcp.py b/python/vyos/ifconfig/dhcp.py
index bf6566c07..57e488cc7 100644
--- a/python/vyos/ifconfig/dhcp.py
+++ b/python/vyos/ifconfig/dhcp.py
@@ -1,151 +1,145 @@
# 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/>.
import os
from vyos.dicts import FixedDict
from vyos.ifconfig.control import Control
from vyos.template import render
config_base = r'/var/lib/dhcp/dhclient_'
class _DHCPv4 (Control):
def __init__(self, ifname):
super().__init__()
self.options = FixedDict(**{
'ifname': ifname,
'hostname': '',
'client_id': '',
'vendor_class_id': '',
'conf_file': config_base + f'{ifname}.conf',
'options_file': config_base + f'{ifname}.options',
'pid_file': config_base + f'{ifname}.pid',
'lease_file': config_base + f'{ifname}.leases',
})
# replace dhcpv4/v6 with systemd.networkd?
def set(self):
"""
Configure interface as DHCP client. The dhclient binary is automatically
started in background!
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
>>> j.dhcp.v4.set()
"""
if not self.options['hostname']:
# read configured system hostname.
# maybe change to vyos hostd client ???
with open('/etc/hostname', 'r') as f:
self.options['hostname'] = f.read().rstrip('\n')
render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options)
render(self.options['conf_file'], 'dhcp-client/ipv4.tmpl', self.options)
return self._cmd('systemctl restart dhclient@{ifname}.service'.format(**self.options))
def delete(self):
"""
De-configure interface as DHCP clinet. All auto generated files like
pid, config and lease will be removed.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
>>> j.dhcp.v4.delete()
"""
if not os.path.isfile(self.options['pid_file']):
self._debug_msg('No DHCP client PID found')
return None
self._cmd('systemctl stop dhclient@{ifname}.service'.format(**self.options))
# cleanup old config files
for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'):
if os.path.isfile(self.options[name]):
os.remove(self.options[name])
class _DHCPv6 (Control):
def __init__(self, ifname):
super().__init__()
self.options = FixedDict(**{
'ifname': ifname,
'conf_file': config_base + f'v6_{ifname}.conf',
'options_file': config_base + f'v6_{ifname}.options',
'pid_file': config_base + f'v6_{ifname}.pid',
'lease_file': config_base + f'v6_{ifname}.leases',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
})
def set(self):
"""
Configure interface as DHCPv6 client. The dhclient binary is automatically
started in background!
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
>>> j.dhcp.v6.set()
"""
# better save then sorry .. should be checked in interface script
# but if you missed it we are safe!
if self.options['dhcpv6_prm_only'] and self.options['dhcpv6_temporary']:
raise Exception(
'DHCPv6 temporary and parameters-only options are mutually exclusive!')
render(self.options['options_file'], 'dhcp-client/daemon-options.tmpl', self.options)
render(self.options['conf_file'], 'dhcp-client/ipv6.tmpl', self.options)
- # no longer accept router announcements on this interface
- self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 0)
-
return self._cmd('systemctl restart dhclient6@{ifname}.service'.format(**self.options))
def delete(self):
"""
De-configure interface as DHCPv6 clinet. All auto generated files like
pid, config and lease will be removed.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
>>> j.dhcp.v6.delete()
"""
if not os.path.isfile(self.options['pid_file']):
self._debug_msg('No DHCPv6 client PID found')
return None
self._cmd('systemctl stop dhclient6@{ifname}.service'.format(**self.options))
- # accept router announcements on this interface
- self._write_sysfs('/proc/sys/net/ipv6/conf/{ifname}/accept_ra'.format(**self.options), 1)
-
# cleanup old config files
for name in ('conf_file', 'options_file', 'pid_file', 'lease_file'):
if os.path.isfile(self.options[name]):
os.remove(self.options[name])
class DHCP(object):
def __init__(self, ifname):
self.v4 = _DHCPv4(ifname)
self.v6 = _DHCPv6(ifname)
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index 7b42e3399..61f2c6482 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -1,740 +1,759 @@
# Copyright 2019-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/>.
import os
import re
import json
from copy import deepcopy
from ipaddress import IPv4Network
from ipaddress import IPv6Address
from ipaddress import IPv6Network
from netifaces import ifaddresses
# this is not the same as socket.AF_INET/INET6
from netifaces import AF_INET
from netifaces import AF_INET6
from vyos import ConfigError
from vyos.util import mac2eui64
from vyos.validate import is_ipv4
from vyos.validate import is_ipv6
from vyos.validate import is_intf_addr_assigned
from vyos.validate import assert_boolean
from vyos.validate import assert_list
from vyos.validate import assert_mac
from vyos.validate import assert_mtu
from vyos.validate import assert_positive
from vyos.validate import assert_range
from vyos.ifconfig.control import Control
from vyos.ifconfig.dhcp import DHCP
from vyos.ifconfig.vrrp import VRRP
from vyos.ifconfig.operational import Operational
from vyos.ifconfig import Section
class Interface(Control):
# This is the class which will be used to create
# self.operational, it allows subclasses, such as
# WireGuard to modify their display behaviour
OperationalClass = Operational
options = ['debug', 'create',]
required = []
default = {
'type': '',
'debug': True,
'create': True,
}
definition = {
'section': '',
'prefixes': [],
'vlan': False,
'bondable': False,
'broadcast': False,
'bridgeable': False,
'eternal': '',
}
_command_get = {
'admin_state': {
'shellcmd': 'ip -json link show dev {ifname}',
'format': lambda j: 'up' if 'UP' in json.loads(j)[0]['flags'] else 'down',
}
}
_command_set = {
'admin_state': {
'validate': lambda v: assert_list(v, ['up', 'down']),
'shellcmd': 'ip link set dev {ifname} {value}',
},
'mac': {
'validate': assert_mac,
'shellcmd': 'ip link set dev {ifname} address {value}',
},
'vrf': {
'convert': lambda v: f'master {v}' if v else 'nomaster',
'shellcmd': 'ip link set dev {ifname} {value}',
},
}
_sysfs_get = {
'alias': {
'location': '/sys/class/net/{ifname}/ifalias',
},
'mac': {
'location': '/sys/class/net/{ifname}/address',
},
'mtu': {
'location': '/sys/class/net/{ifname}/mtu',
},
'oper_state':{
'location': '/sys/class/net/{ifname}/operstate',
},
}
_sysfs_set = {
'alias': {
'convert': lambda name: name if name else '\0',
'location': '/sys/class/net/{ifname}/ifalias',
},
'mtu': {
'validate': assert_mtu,
'location': '/sys/class/net/{ifname}/mtu',
},
'arp_cache_tmo': {
'convert': lambda tmo: (int(tmo) * 1000),
'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms',
},
'arp_filter': {
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter',
},
'arp_accept': {
'validate': lambda arp: assert_range(arp,0,2),
'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept',
},
'arp_announce': {
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce',
},
'arp_ignore': {
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore',
},
+ 'ipv6_accept_ra': {
+ 'validate': lambda ara: assert_range(ara,0,3),
+ 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra',
+ },
'ipv6_autoconf': {
- 'validate': lambda fwd: assert_range(fwd,0,2),
+ 'validate': lambda aco: assert_range(aco,0,2),
'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf',
},
'ipv6_forwarding': {
'validate': lambda fwd: assert_range(fwd,0,2),
'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding',
},
'ipv6_dad_transmits': {
'validate': assert_positive,
'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits',
},
'proxy_arp': {
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp',
},
'proxy_arp_pvlan': {
'validate': assert_boolean,
'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan',
},
# link_detect vs link_filter name weirdness
'link_detect': {
'validate': lambda link: assert_range(link,0,3),
'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter',
},
}
@classmethod
def exists(cls, ifname):
return os.path.exists(f'/sys/class/net/{ifname}')
def __init__(self, ifname, **kargs):
"""
This is the base interface class which supports basic IP/MAC address
operations as well as DHCP(v6). Other interface which represent e.g.
and ethernet bridge are implemented as derived classes adding all
additional functionality.
For creation you will need to provide the interface type, otherwise
the existing interface is used
DEBUG:
This class has embedded debugging (print) which can be enabled by
creating the following file:
vyos@vyos# touch /tmp/vyos.ifconfig.debug
Example:
>>> from vyos.ifconfig import Interface
>>> i = Interface('eth0')
"""
self.config = deepcopy(self.default)
for k in self.options:
if k in kargs:
self.config[k] = kargs[k]
# make sure the ifname is the first argument and not from the dict
self.config['ifname'] = ifname
# we must have updated config before initialising the Interface
super().__init__(**kargs)
self.ifname = ifname
self.dhcp = DHCP(ifname)
if not self.exists(ifname):
# Any instance of Interface, such as Interface('eth0')
# can be used safely to access the generic function in this class
# as 'type' is unset, the class can not be created
if not self.config['type']:
raise Exception(f'interface "{ifname}" not found')
# Should an Instance of a child class (EthernetIf, DummyIf, ..)
# be required, then create should be set to False to not accidentally create it.
# In case a subclass does not define it, we use get to set the default to True
if self.config.get('create',True):
for k in self.required:
if k not in kargs:
name = self.default['type']
raise ConfigError(f'missing required option {k} for {name} {ifname} creation')
self._create()
# If we can not connect to the interface then let the caller know
# as the class could not be correctly initialised
else:
raise Exception('interface "{}" not found'.format(self.config['ifname']))
# temporary list of assigned IP addresses
self._addr = []
self.operational = self.OperationalClass(ifname)
self.vrrp = VRRP(ifname)
def _create(self):
cmd = 'ip link add dev {ifname} type {type}'.format(**self.config)
self._cmd(cmd)
def remove(self):
"""
Remove interface from operating system. Removing the interface
deconfigures all assigned IP addresses and clear possible DHCP(v6)
client processes.
Example:
>>> from vyos.ifconfig import Interface
>>> i = Interface('eth0')
>>> i.remove()
"""
# remove all assigned IP addresses from interface - this is a bit redundant
# as the kernel will remove all addresses on interface deletion, but we
# can not delete ALL interfaces, see below
self.flush_addrs()
# ---------------------------------------------------------------------
# Any class can define an eternal regex in its definition
# interface matching the regex will not be deleted
eternal = self.definition['eternal']
if not eternal:
self._delete()
elif not re.match(eternal, self.ifname):
self._delete()
def _delete(self):
# 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'])
return self._cmd(cmd)
def get_mtu(self):
"""
Get/set interface mtu in bytes.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').get_mtu()
'1500'
"""
return self.get_interface('mtu')
def set_mtu(self, mtu):
"""
Get/set interface mtu in bytes.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_mtu(1400)
>>> Interface('eth0').get_mtu()
'1400'
"""
return self.set_interface('mtu', mtu)
def get_mac(self):
"""
Get current interface MAC (Media Access Contrl) address used.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').get_mac()
'00:50:ab:cd:ef:00'
"""
return self.get_interface('mac')
def set_mac(self, mac):
"""
Set interface MAC (Media Access Contrl) address to given value.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_mac('00:50:ab:cd:ef:01')
"""
# If MAC is unchanged, bail out early
if mac == self.get_mac():
return None
# MAC address can only be changed if interface is in 'down' state
prev_state = self.get_admin_state()
if prev_state == 'up':
self.set_admin_state('down')
self.set_interface('mac', mac)
def set_vrf(self, vrf=''):
"""
Add/Remove interface from given VRF instance.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_vrf('foo')
>>> Interface('eth0').set_vrf()
"""
self.set_interface('vrf', vrf)
def set_arp_cache_tmo(self, tmo):
"""
Set ARP cache timeout value in seconds. Internal Kernel representation
is in milliseconds.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_arp_cache_tmo(40)
"""
return self.set_interface('arp_cache_tmo', tmo)
def set_arp_filter(self, arp_filter):
"""
Filter ARP requests
1 - Allows you to have multiple network interfaces on the same
subnet, and have the ARPs for each interface be answered
based on whether or not the kernel would route a packet from
the ARP'd IP out that interface (therefore you must use source
based routing for this to work). In other words it allows control
of which cards (usually 1) will respond to an arp request.
0 - (default) The kernel can respond to arp requests with addresses
from other interfaces. This may seem wrong but it usually makes
sense, because it increases the chance of successful communication.
IP addresses are owned by the complete host on Linux, not by
particular interfaces. Only for more complex setups like load-
balancing, does this behaviour cause problems.
"""
return self.set_interface('arp_filter', arp_filter)
def set_arp_accept(self, arp_accept):
"""
Define behavior for gratuitous ARP frames who's IP is not
already present in the ARP table:
0 - don't create new entries in the ARP table
1 - create new entries in the ARP table
Both replies and requests type gratuitous arp will trigger the
ARP table to be updated, if this setting is on.
If the ARP table already contains the IP address of the
gratuitous arp frame, the arp table will be updated regardless
if this setting is on or off.
"""
return self.set_interface('arp_accept', arp_accept)
def set_arp_announce(self, arp_announce):
"""
Define different restriction levels for announcing the local
source IP address from IP packets in ARP requests sent on
interface:
0 - (default) Use any local address, configured on any interface
1 - Try to avoid local addresses that are not in the target's
subnet for this interface. This mode is useful when target
hosts reachable via this interface require the source IP
address in ARP requests to be part of their logical network
configured on the receiving interface. When we generate the
request we will check all our subnets that include the
target IP and will preserve the source address if it is from
such subnet.
Increasing the restriction level gives more chance for
receiving answer from the resolved target while decreasing
the level announces more valid sender's information.
"""
return self.set_interface('arp_announce', arp_announce)
def set_arp_ignore(self, arp_ignore):
"""
Define different modes for sending replies in response to received ARP
requests that resolve local target IP addresses:
0 - (default): reply for any local target IP address, configured
on any interface
1 - reply only if the target IP address is local address
configured on the incoming interface
"""
return self.set_interface('arp_ignore', arp_ignore)
+ def set_ipv6_accept_ra(self, accept_ra):
+ """
+ Accept Router Advertisements; autoconfigure using them.
+
+ It also determines whether or not to transmit Router Solicitations.
+ If and only if the functional setting is to accept Router
+ Advertisements, Router Solicitations will be transmitted.
+
+ 0 - Do not accept Router Advertisements.
+ 1 - (default) Accept Router Advertisements if forwarding is disabled.
+ 2 - Overrule forwarding behaviour. Accept Router Advertisements even if
+ forwarding is enabled.
+ """
+ return self.set_interface('ipv6_accept_ra', accept_ra)
+
def set_ipv6_autoconf(self, autoconf):
"""
Autoconfigure addresses using Prefix Information in Router
Advertisements.
"""
return self.set_interface('ipv6_autoconf', autoconf)
def add_ipv6_eui64_address(self, prefix):
"""
Extended Unique Identifier (EUI), as per RFC2373, allows a host to
assign itself a unique IPv6 address based on a given IPv6 prefix.
Calculate the EUI64 from the interface's MAC, then assign it
with the given prefix to the interface.
"""
eui64 = mac2eui64(self.get_mac(), prefix)
prefixlen = prefix.split('/')[1]
self.add_addr(f'{eui64}/{prefixlen}')
def del_ipv6_eui64_address(self, prefix):
"""
Delete the address based on the interface's MAC-based EUI64
combined with the prefix address.
"""
eui64 = mac2eui64(self.get_mac(), prefix)
prefixlen = prefix.split('/')[1]
self.del_addr(f'{eui64}/{prefixlen}')
def set_ipv6_forwarding(self, forwarding):
"""
Configure IPv6 interface-specific Host/Router behaviour.
False:
By default, Host behaviour is assumed. This means:
1. IsRouter flag is not set in Neighbour Advertisements.
2. If accept_ra is TRUE (default), transmit Router
Solicitations.
3. If accept_ra is TRUE (default), accept Router
Advertisements (and do autoconfiguration).
4. If accept_redirects is TRUE (default), accept Redirects.
True:
If local forwarding is enabled, Router behaviour is assumed.
This means exactly the reverse from the above:
1. IsRouter flag is set in Neighbour Advertisements.
2. Router Solicitations are not sent unless accept_ra is 2.
3. Router Advertisements are ignored unless accept_ra is 2.
4. Redirects are ignored.
"""
return self.set_interface('ipv6_forwarding', forwarding)
def set_ipv6_dad_messages(self, dad):
"""
The amount of Duplicate Address Detection probes to send.
Default: 1
"""
return self.set_interface('ipv6_dad_transmits', dad)
def set_link_detect(self, link_filter):
"""
Configure kernel response in packets received on interfaces that are 'down'
0 - Allow packets to be received for the address on this interface
even if interface is disabled or no carrier.
1 - Ignore packets received if interface associated with the incoming
address is down.
2 - Ignore packets received if interface associated with the incoming
address is down or has no carrier.
Default value is 0. Note that some distributions enable it in startup
scripts.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_link_detect(1)
"""
return self.set_interface('link_detect', link_filter)
def get_alias(self):
"""
Get interface alias name used by e.g. SNMP
Example:
>>> Interface('eth0').get_alias()
'interface description as set by user'
"""
return self.get_interface('alias')
def set_alias(self, ifalias=''):
"""
Set interface alias name used by e.g. SNMP
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_alias('VyOS upstream interface')
to clear alias e.g. delete it use:
>>> Interface('eth0').set_ifalias('')
"""
self.set_interface('alias', ifalias)
def get_admin_state(self):
"""
Get interface administrative state. Function will return 'up' or 'down'
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').get_admin_state()
'up'
"""
return self.get_interface('admin_state')
def set_admin_state(self, state):
"""
Set interface administrative state to be 'up' or 'down'
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_admin_state('down')
>>> Interface('eth0').get_admin_state()
'down'
"""
return self.set_interface('admin_state', state)
def set_proxy_arp(self, enable):
"""
Set per interface proxy ARP configuration
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_proxy_arp(1)
"""
self.set_interface('proxy_arp', enable)
def set_proxy_arp_pvlan(self, enable):
"""
Private VLAN proxy arp.
Basically allow proxy arp replies back to the same interface
(from which the ARP request/solicitation was received).
This is done to support (ethernet) switch features, like RFC
3069, where the individual ports are NOT allowed to
communicate with each other, but they are allowed to talk to
the upstream router. As described in RFC 3069, it is possible
to allow these hosts to communicate through the upstream
router by proxy_arp'ing. Don't need to be used together with
proxy_arp.
This technology is known by different names:
In RFC 3069 it is called VLAN Aggregation.
Cisco and Allied Telesyn call it Private VLAN.
Hewlett-Packard call it Source-Port filtering or port-isolation.
Ericsson call it MAC-Forced Forwarding (RFC Draft).
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_proxy_arp_pvlan(1)
"""
self.set_interface('proxy_arp_pvlan', enable)
def get_addr(self):
"""
Retrieve assigned IPv4 and IPv6 addresses from given interface.
This is done using the netifaces and ipaddress python modules.
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').get_addrs()
['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64']
"""
ipv4 = []
ipv6 = []
if AF_INET in ifaddresses(self.config['ifname']).keys():
for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]:
# we need to manually assemble a list of IPv4 address/prefix
prefix = '/' + \
str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen)
ipv4.append(v4_addr['addr'] + prefix)
if AF_INET6 in ifaddresses(self.config['ifname']).keys():
for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]:
# Note that currently expanded netmasks are not supported. That means
# 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not.
# see https://docs.python.org/3/library/ipaddress.html
bits = bin(
int(v6_addr['netmask'].replace(':', ''), 16)).count('1')
prefix = '/' + str(bits)
# we alsoneed to remove the interface suffix on link local
# addresses
v6_addr['addr'] = v6_addr['addr'].split('%')[0]
ipv6.append(v6_addr['addr'] + prefix)
return ipv4 + ipv6
def add_addr(self, addr):
"""
Add IP(v6) address to interface. Address is only added if it is not
already assigned to that interface. Address format must be validated
and compressed/normalized before calling this function.
addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
IPv4: add IPv4 address to interface
IPv6: add IPv6 address to interface
dhcp: start dhclient (IPv4) on interface
dhcpv6: start dhclient (IPv6) on interface
Returns False if address is already assigned and wasn't re-added.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
>>> j.add_addr('192.0.2.1/24')
>>> j.add_addr('2001:db8::ffff/64')
>>> j.get_addr()
['192.0.2.1/24', '2001:db8::ffff/64']
"""
# XXX: normalize/compress with ipaddress if calling functions don't?
# is subnet mask always passed, and in the same way?
# do not add same address twice
if addr in self._addr:
return False
# we can't have both DHCP and static IPv4 addresses assigned
for a in self._addr:
if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or
( a == 'dhcp' and addr != 'dhcpv6' and is_ipv4(addr) ) ):
raise ConfigError((
"Can't configure both static IPv4 and DHCP address "
"on the same interface"))
# add to interface
if addr == 'dhcp':
self.dhcp.v4.set()
elif addr == 'dhcpv6':
self.dhcp.v6.set()
elif not is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr add "{addr}" dev "{self.ifname}"')
else:
return False
# add to cache
self._addr.append(addr)
return True
def del_addr(self, addr):
"""
Delete IP(v6) address from interface. Address is only deleted if it is
assigned to that interface. Address format must be exactly the same as
was used when adding the address.
addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6!
IPv4: delete IPv4 address from interface
IPv6: delete IPv6 address from interface
dhcp: stop dhclient (IPv4) on interface
dhcpv6: stop dhclient (IPv6) on interface
Returns False if address isn't already assigned and wasn't deleted.
Example:
>>> from vyos.ifconfig import Interface
>>> j = Interface('eth0')
>>> j.add_addr('2001:db8::ffff/64')
>>> j.add_addr('192.0.2.1/24')
>>> j.get_addr()
['192.0.2.1/24', '2001:db8::ffff/64']
>>> j.del_addr('192.0.2.1/24')
>>> j.get_addr()
['2001:db8::ffff/64']
"""
# remove from interface
if addr == 'dhcp':
self.dhcp.v4.delete()
elif addr == 'dhcpv6':
self.dhcp.v6.delete()
elif is_intf_addr_assigned(self.ifname, addr):
self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"')
else:
return False
# remove from cache
if addr in self._addr:
self._addr.remove(addr)
return True
def flush_addrs(self):
"""
Flush all addresses from an interface, including DHCP.
Will raise an exception on error.
"""
# stop DHCP(v6) if running
self.dhcp.v4.delete()
self.dhcp.v6.delete()
# flush all addresses
self._cmd(f'ip addr flush dev "{self.ifname}"')
def add_to_bridge(self, br):
"""
Adds the interface to the bridge with the passed port config.
Returns False if bridge doesn't exist.
"""
# check if the bridge exists (on boot it doesn't)
if br not in Section.interfaces('bridge'):
return False
self.flush_addrs()
# add interface to bridge - use Section.klass to get BridgeIf class
Section.klass(br)(br, create=False).add_port(self.ifname)
# TODO: port config (STP)
return True
diff --git a/python/vyos/ifconfig_vlan.py b/python/vyos/ifconfig_vlan.py
index bb93121e7..6410be9aa 100644
--- a/python/vyos/ifconfig_vlan.py
+++ b/python/vyos/ifconfig_vlan.py
@@ -1,237 +1,239 @@
# Copyright 2019-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 netifaces import interfaces
from vyos import ConfigError
def apply_all_vlans(intf, intfconfig):
"""
Function applies all VLANs to the passed interface.
intf: object of Interface class
intfconfig: dict with interface configuration
"""
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in intfconfig['vif_s_remove']:
intf.del_vlan(vif_s)
# create service VLAN interfaces (vif-s)
for vif_s_id, vif_s in intfconfig['vif_s'].items():
s_vlan = intf.add_vlan(vif_s_id, ethertype=vif_s['ethertype'])
apply_vlan_config(s_vlan, vif_s)
# remove no longer required client VLAN interfaces (vif-c)
# on lower service VLAN interface
for vif_c in intfconfig['vif_c_remove']:
s_vlan.del_vlan(vif_c)
# create client VLAN interfaces (vif-c)
# on lower service VLAN interface
for vif_c_id, vif_c in vif_s['vif_c'].items():
c_vlan = s_vlan.add_vlan(vif_c_id)
apply_vlan_config(c_vlan, vif_c)
# remove no longer required VLAN interfaces (vif)
for vif in intfconfig['vif_remove']:
intf.del_vlan(vif)
# create VLAN interfaces (vif)
for vif_id, vif in intfconfig['vif'].items():
# QoS priority mapping can only be set during interface creation
# so we delete the interface first if required.
if vif['egress_qos_changed'] or vif['ingress_qos_changed']:
try:
# on system bootup the above condition is true but the interface
# does not exists, which throws an exception, but that's legal
intf.del_vlan(vif_id)
except:
pass
vlan = intf.add_vlan(vif_id, ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
apply_vlan_config(vlan, vif)
def apply_vlan_config(vlan, config):
"""
Generic function to apply a VLAN configuration from a dictionary
to a VLAN interface
"""
if not vlan.definition['vlan']:
raise TypeError()
if config['dhcp_client_id']:
vlan.dhcp.v4.options['client_id'] = config['dhcp_client_id']
if config['dhcp_hostname']:
vlan.dhcp.v4.options['hostname'] = config['dhcp_hostname']
if config['dhcp_vendor_class_id']:
vlan.dhcp.v4.options['vendor_class_id'] = config['dhcp_vendor_class_id']
if config['dhcpv6_prm_only']:
vlan.dhcp.v6.options['dhcpv6_prm_only'] = True
if config['dhcpv6_temporary']:
vlan.dhcp.v6.options['dhcpv6_temporary'] = True
# update interface description used e.g. within SNMP
vlan.set_alias(config['description'])
# ignore link state changes
vlan.set_link_detect(config['disable_link_detect'])
# configure ARP filter configuration
vlan.set_arp_filter(config['ip_disable_arp_filter'])
# configure ARP accept
vlan.set_arp_accept(config['ip_enable_arp_accept'])
# configure ARP announce
vlan.set_arp_announce(config['ip_enable_arp_announce'])
# configure ARP ignore
vlan.set_arp_ignore(config['ip_enable_arp_ignore'])
# configure Proxy ARP
vlan.set_proxy_arp(config['ip_proxy_arp'])
+ # IPv6 accept RA
+ vlan.set_ipv6_accept_ra(config['ipv6_accept_ra'])
# IPv6 address autoconfiguration
vlan.set_ipv6_autoconf(config['ipv6_autoconf'])
# IPv6 forwarding
vlan.set_ipv6_forwarding(config['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
vlan.set_ipv6_dad_messages(config['ipv6_dup_addr_detect'])
# Maximum Transmission Unit (MTU)
vlan.set_mtu(config['mtu'])
# assign/remove VRF (ONLY when not a member of a bridge,
# otherwise 'nomaster' removes it from it)
if not config['is_bridge_member']:
vlan.set_vrf(config['vrf'])
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in config['ipv6_eui64_prefix_remove']:
vlan.del_ipv6_eui64_address(addr)
# Change VLAN interface MAC address
if config['mac']:
vlan.set_mac(config['mac'])
# Add IPv6 EUI-based addresses
for addr in config['ipv6_eui64_prefix']:
vlan.add_ipv6_eui64_address(addr)
# enable/disable VLAN interface
if config['disable']:
vlan.set_admin_state('down')
else:
vlan.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in config['address_remove']:
vlan.del_addr(addr)
for addr in config['address']:
vlan.add_addr(addr)
# re-add ourselves to any bridge we might have fallen out of
if config['is_bridge_member']:
vlan.add_to_bridge(config['is_bridge_member'])
def verify_vlan_config(config):
"""
Generic function to verify VLAN config consistency. Instead of re-
implementing this function in multiple places use single source \o/
"""
# config['vif'] is a dict with ids as keys and config dicts as values
for vif in config['vif'].values():
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif['dhcpv6_prm_only'] and vif['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
if ( vif['is_bridge_member']
and ( vif['address']
or vif['ipv6_eui64_prefix']
or vif['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to vif interface {vif["intf"]} '
f'which is a member of bridge {vif["is_bridge_member"]}'))
if vif['vrf']:
if vif['vrf'] not in interfaces():
raise ConfigError(f'VRF "{vif["vrf"]}" does not exist')
if vif['is_bridge_member']:
raise ConfigError((
f'vif {vif["intf"]} cannot be member of VRF {vif["vrf"]} '
f'and bridge {vif["is_bridge_member"]} at the same time!'))
# e.g. wireless interface has no vif_s support
# thus we bail out eraly.
if 'vif_s' not in config.keys():
return
# config['vif_s'] is a dict with ids as keys and config dicts as values
for vif_s_id, vif_s in config['vif_s'].items():
for vif_id, vif in config['vif'].items():
if vif_id == vif_s_id:
raise ConfigError((
f'Cannot use identical ID on vif "{vif["intf"]}" '
f'and vif-s "{vif_s["intf"]}"'))
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif_s['dhcpv6_prm_only'] and vif_s['dhcpv6_temporary']:
raise ConfigError((
'DHCPv6 temporary and parameters-only options are mutually '
'exclusive!'))
if ( vif_s['is_bridge_member']
and ( vif_s['address']
or vif_s['ipv6_eui64_prefix']
or vif_s['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to vif-s interface {vif_s["intf"]} '
f'which is a member of bridge {vif_s["is_bridge_member"]}'))
if vif_s['vrf']:
if vif_s['vrf'] not in interfaces():
raise ConfigError(f'VRF "{vif_s["vrf"]}" does not exist')
if vif_s['is_bridge_member']:
raise ConfigError((
f'vif-s {vif_s["intf"]} cannot be member of VRF {vif_s["vrf"]} '
f'and bridge {vif_s["is_bridge_member"]} at the same time!'))
# vif_c is a dict with ids as keys and config dicts as values
for vif_c in vif_s['vif_c'].values():
# DHCPv6 parameters-only and temporary address are mutually exclusive
if vif_c['dhcpv6_prm_only'] and vif_c['dhcpv6_temporary']:
raise ConfigError((
'DHCPv6 temporary and parameters-only options are '
'mutually exclusive!'))
if ( vif_c['is_bridge_member']
and ( vif_c['address']
or vif_c['ipv6_eui64_prefix']
or vif_c['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to vif-c interface {vif_c["intf"]} '
f'which is a member of bridge {vif_c["is_bridge_member"]}'))
if vif_c['vrf']:
if vif_c['vrf'] not in interfaces():
raise ConfigError(f'VRF "{vif_c["vrf"]}" does not exist')
if vif_c['is_bridge_member']:
raise ConfigError((
f'vif-c {vif_c["intf"]} cannot be member of VRF {vif_c["vrf"]} '
f'and bridge {vif_c["is_bridge_member"]} at the same time!'))
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 5a2ff9eef..93d2adbd6 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -1,427 +1,430 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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.ifconfig import BondIf
from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.config import Config
from vyos.util import call, cmd
from vyos.validate import is_member, has_address_configured
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
'arp_mon_intvl': 0,
'arp_mon_tgt': [],
'description': '',
'deleted': False,
'dhcp_client_id': '',
'dhcp_hostname': '',
'dhcp_vendor_class_id': '',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
'disable': False,
'disable_link_detect': 1,
'hash_policy': 'layer2',
'intf': '',
'ip_arp_cache_tmo': 30,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
'mac': '',
'mode': '802.3ad',
'member': [],
'shutdown_required': False,
'mtu': 1500,
'primary': '',
'vif_s': {},
'vif_s_remove': [],
'vif': {},
'vif_remove': [],
'vrf': ''
}
def get_bond_mode(mode):
if mode == 'round-robin':
return 'balance-rr'
elif mode == 'active-backup':
return 'active-backup'
elif mode == 'xor-hash':
return 'balance-xor'
elif mode == 'broadcast':
return 'broadcast'
elif mode == '802.3ad':
return '802.3ad'
elif mode == 'transmit-load-balance':
return 'balance-tlb'
elif mode == 'adaptive-load-balance':
return 'balance-alb'
else:
raise ConfigError('invalid bond mode "{}"'.format(mode))
def get_config():
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
ifname = os.environ['VYOS_TAGNODE_VALUE']
conf = Config()
# initialize kernel module if not loaded
if not os.path.isfile('/sys/class/net/bonding_masters'):
import syslog
syslog.syslog(syslog.LOG_NOTICE, "loading bonding kernel module")
if call('modprobe bonding max_bonds=0 miimon=250') != 0:
syslog.syslog(syslog.LOG_NOTICE, "failed loading bonding kernel module")
raise ConfigError("failed loading bonding kernel module")
# check if bond has been removed
cfg_base = 'interfaces bonding ' + ifname
if not conf.exists(cfg_base):
bond = deepcopy(default_config_data)
bond['intf'] = ifname
bond['deleted'] = True
return bond
# set new configuration level
conf.set_level(cfg_base)
bond, disabled = intf_to_dict(conf, default_config_data)
# ARP link monitoring frequency in milliseconds
if conf.exists('arp-monitor interval'):
bond['arp_mon_intvl'] = int(conf.return_value('arp-monitor interval'))
# IP address to use for ARP monitoring
if conf.exists('arp-monitor target'):
bond['arp_mon_tgt'] = conf.return_values('arp-monitor target')
# Bonding transmit hash policy
if conf.exists('hash-policy'):
bond['hash_policy'] = conf.return_value('hash-policy')
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
bond['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
bond['ip_proxy_arp_pvlan'] = 1
# Bonding mode
if conf.exists('mode'):
act_mode = conf.return_value('mode')
eff_mode = conf.return_effective_value('mode')
if not (act_mode == eff_mode):
bond['shutdown_required'] = True
bond['mode'] = get_bond_mode(act_mode)
# determine bond member interfaces (currently configured)
if conf.exists('member interface'):
bond['member'] = conf.return_values('member interface')
# We can not call conf.return_effective_values() as it would not work
# on reboots. Reboots/First boot will return that running config and
# saved config is the same, thus on a reboot the bond members will
# not be added all (https://phabricator.vyos.net/T2030)
live_members = BondIf(bond['intf']).get_slaves()
if not (bond['member'] == live_members):
bond['shutdown_required'] = True
# Primary device interface
if conf.exists('primary'):
bond['primary'] = conf.return_value('primary')
add_to_dict(conf, disabled, bond, 'vif', 'vif')
add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s')
return bond
def verify(bond):
if bond['deleted']:
if bond['is_bridge_member']:
raise ConfigError((
f'Cannot delete interface "{bond["intf"]}" as it is a '
f'member of bridge "{bond["is_bridge_member"]}"!'))
return None
if len(bond['arp_mon_tgt']) > 16:
raise ConfigError('The maximum number of arp-monitor targets is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError((
'Mode dependency failed, primary not supported in mode '
f'"{bond["mode"]}"!'))
if ( bond['is_bridge_member']
and ( bond['address']
or bond['ipv6_eui64_prefix']
or bond['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{bond["intf"]}" '
f'as it is a member of bridge "{bond["is_bridge_member"]}"!'))
if bond['vrf']:
if bond['vrf'] not in interfaces():
raise ConfigError(f'VRF "{bond["vrf"]}" does not exist')
if bond['is_bridge_member']:
raise ConfigError((
f'Interface "{bond["intf"]}" cannot be member of VRF '
f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} '
f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(bond)
conf = Config()
for intf in bond['member']:
# check if member interface is "real"
if intf not in interfaces():
raise ConfigError(f'Interface {intf} does not exist!')
# a bonding member interface is only allowed to be assigned to one bond!
all_bonds = conf.list_nodes('interfaces bonding')
# We do not need to check our own bond
all_bonds.remove(bond['intf'])
for tmp in all_bonds:
if conf.exists('interfaces bonding {tmp} member interface {intf}'):
raise ConfigError((
f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
f'it is already a member of bond "{tmp}"!'))
# can not add interfaces with an assigned address to a bond
if has_address_configured(conf, intf):
raise ConfigError((
f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
f'it has an address assigned!'))
# bond members are not allowed to be bridge members
tmp = is_member(conf, intf, 'bridge')
if tmp:
raise ConfigError((
f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
f'it is already a member of bridge "{tmp}"!'))
# bond members are not allowed to be vrrp members
for tmp in conf.list_nodes('high-availability vrrp group'):
if conf.exists('high-availability vrrp group {tmp} interface {intf}'):
raise ConfigError((
f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
f'it is already a member of VRRP group "{tmp}"!'))
# bond members are not allowed to be underlaying psuedo-ethernet devices
for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'):
raise ConfigError((
f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
f'it is already the link of pseudo-ethernet "{tmp}"!'))
# bond members are not allowed to be underlaying vxlan devices
for tmp in conf.list_nodes('interfaces vxlan'):
if conf.exists('interfaces vxlan {tmp} link {intf}'):
raise ConfigError((
f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
f'it is already the link of VXLAN "{tmp}"!'))
if bond['primary']:
if bond['primary'] not in bond['member']:
raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member')
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('primary interface only works for mode active-backup, ' \
'transmit-load-balance or adaptive-load-balance')
if bond['arp_mon_intvl'] > 0:
if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
'transmit-load-balance or adaptive-load-balance')
return None
def generate(bond):
return None
def apply(bond):
b = BondIf(bond['intf'])
if bond['deleted']:
# delete interface
b.remove()
else:
# ARP link monitoring frequency, reset miimon when arp-montior is inactive
# this is done inside BondIf automatically
b.set_arp_interval(bond['arp_mon_intvl'])
# ARP monitor targets need to be synchronized between sysfs and CLI.
# Unfortunately an address can't be send twice to sysfs as this will
# result in the following exception: OSError: [Errno 22] Invalid argument.
#
# We remove ALL adresses prior adding new ones, this will remove addresses
# added manually by the user too - but as we are limited to 16 adresses
# from the kernel side this looks valid to me. We won't run into an error
# when a user added manual adresses which would result in having more
# then 16 adresses in total.
arp_tgt_addr = list(map(str, b.get_arp_ip_target().split()))
for addr in arp_tgt_addr:
b.set_arp_ip_target('-' + addr)
# Add configured ARP target addresses
for addr in bond['arp_mon_tgt']:
b.set_arp_ip_target('+' + addr)
# update interface description used e.g. within SNMP
b.set_alias(bond['description'])
if bond['dhcp_client_id']:
b.dhcp.v4.options['client_id'] = bond['dhcp_client_id']
if bond['dhcp_hostname']:
b.dhcp.v4.options['hostname'] = bond['dhcp_hostname']
if bond['dhcp_vendor_class_id']:
b.dhcp.v4.options['vendor_class_id'] = bond['dhcp_vendor_class_id']
if bond['dhcpv6_prm_only']:
b.dhcp.v6.options['dhcpv6_prm_only'] = True
if bond['dhcpv6_temporary']:
b.dhcp.v6.options['dhcpv6_temporary'] = True
# ignore link state changes
b.set_link_detect(bond['disable_link_detect'])
# Bonding transmit hash policy
b.set_hash_policy(bond['hash_policy'])
# configure ARP cache timeout in milliseconds
b.set_arp_cache_tmo(bond['ip_arp_cache_tmo'])
# configure ARP filter configuration
b.set_arp_filter(bond['ip_disable_arp_filter'])
# configure ARP accept
b.set_arp_accept(bond['ip_enable_arp_accept'])
# configure ARP announce
b.set_arp_announce(bond['ip_enable_arp_announce'])
# configure ARP ignore
b.set_arp_ignore(bond['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
b.set_proxy_arp(bond['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ b.set_ipv6_accept_ra(bond['ipv6_accept_ra'])
# IPv6 address autoconfiguration
b.set_ipv6_autoconf(bond['ipv6_autoconf'])
# IPv6 forwarding
b.set_ipv6_forwarding(bond['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
b.set_ipv6_dad_messages(bond['ipv6_dup_addr_detect'])
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in bond['ipv6_eui64_prefix_remove']:
b.del_ipv6_eui64_address(addr)
# Change interface MAC address
if bond['mac']:
b.set_mac(bond['mac'])
# Add IPv6 EUI-based addresses
for addr in bond['ipv6_eui64_prefix']:
b.add_ipv6_eui64_address(addr)
# Maximum Transmission Unit (MTU)
b.set_mtu(bond['mtu'])
# Primary device interface
if bond['primary']:
b.set_primary(bond['primary'])
# Some parameters can not be changed when the bond is up.
if bond['shutdown_required']:
# Disable bond prior changing of certain properties
b.set_admin_state('down')
# The bonding mode can not be changed when there are interfaces enslaved
# to this bond, thus we will free all interfaces from the bond first!
for intf in b.get_slaves():
b.del_port(intf)
# Bonding policy/mode
b.set_mode(bond['mode'])
# Add (enslave) interfaces to bond
for intf in bond['member']:
# if we've come here we already verified the interface doesn't
# have addresses configured so just flush any remaining ones
cmd(f'ip addr flush dev "{intf}"')
b.add_port(intf)
# As the bond interface is always disabled first when changing
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not bond['disable']:
b.set_admin_state('up')
else:
b.set_admin_state('down')
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in bond['address_remove']:
b.del_addr(addr)
for addr in bond['address']:
b.add_addr(addr)
# assign/remove VRF (ONLY when not a member of a bridge,
# otherwise 'nomaster' removes it from it)
if not bond['is_bridge_member']:
b.set_vrf(bond['vrf'])
# re-add ourselves to any bridge we might have fallen out of
if bond['is_bridge_member']:
b.add_to_bridge(bond['is_bridge_member'])
# apply all vlans to interface
apply_all_vlans(b, bond)
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/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index c43fae78b..217ba95e1 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -1,400 +1,408 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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.ifconfig import BridgeIf, Section
from vyos.ifconfig.stp import STP
from vyos.configdict import list_diff
from vyos.validate import is_member, has_address_configured
from vyos.config import Config
from vyos.util import cmd, get_bridge_member_config
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
'aging': 300,
'arp_cache_tmo': 30,
'description': '',
'deleted': False,
'dhcp_client_id': '',
'dhcp_hostname': '',
'dhcp_vendor_class_id': '',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
'disable': False,
'disable_link_detect': 1,
'forwarding_delay': 14,
'hello_time': 2,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
'intf': '',
'mac' : '',
'max_age': 20,
'member': [],
'member_remove': [],
'priority': 32768,
'stp': 0,
'vrf': ''
}
def get_config():
bridge = 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')
bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if bridge has been removed
if not conf.exists('interfaces bridge ' + bridge['intf']):
bridge['deleted'] = True
return bridge
# set new configuration level
conf.set_level('interfaces bridge ' + bridge['intf'])
# retrieve configured interface addresses
if conf.exists('address'):
bridge['address'] = conf.return_values('address')
# Determine interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values('address')
bridge['address_remove'] = list_diff(eff_addr, bridge['address'])
# retrieve aging - how long addresses are retained
if conf.exists('aging'):
bridge['aging'] = int(conf.return_value('aging'))
# retrieve interface description
if conf.exists('description'):
bridge['description'] = conf.return_value('description')
# get DHCP client identifier
if conf.exists('dhcp-options client-id'):
bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id')
# DHCP client host name (overrides the system host name)
if conf.exists('dhcp-options host-name'):
bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
# DHCP client vendor identifier
if conf.exists('dhcp-options vendor-class-id'):
bridge['dhcp_vendor_class_id'] = conf.return_value('dhcp-options vendor-class-id')
# DHCPv6 only acquire config parameters, no address
if conf.exists('dhcpv6-options parameters-only'):
bridge['dhcpv6_prm_only'] = True
# DHCPv6 temporary IPv6 address
if conf.exists('dhcpv6-options temporary'):
bridge['dhcpv6_temporary'] = True
# Disable this bridge interface
if conf.exists('disable'):
bridge['disable'] = True
# Ignore link state changes
if conf.exists('disable-link-detect'):
bridge['disable_link_detect'] = 2
# Forwarding delay
if conf.exists('forwarding-delay'):
bridge['forwarding_delay'] = int(conf.return_value('forwarding-delay'))
# Hello packet advertisment interval
if conf.exists('hello-time'):
bridge['hello_time'] = int(conf.return_value('hello-time'))
# Enable Internet Group Management Protocol (IGMP) querier
if conf.exists('igmp querier'):
bridge['igmp_querier'] = 1
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
bridge['arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# ARP filter configuration
if conf.exists('ip disable-arp-filter'):
bridge['ip_disable_arp_filter'] = 0
# ARP enable accept
if conf.exists('ip enable-arp-accept'):
bridge['ip_enable_arp_accept'] = 1
# ARP enable announce
if conf.exists('ip enable-arp-announce'):
bridge['ip_enable_arp_announce'] = 1
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
bridge['ip_enable_arp_ignore'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
bridge['ipv6_autoconf'] = 1
# Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Determine currently effective EUI64 addresses - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values('ipv6 address eui64')
bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix'])
# Remove the default link-local address if set.
if conf.exists('ipv6 address no-default-link-local'):
bridge['ipv6_eui64_prefix_remove'].append('fe80::/64')
else:
# add the link-local by default to make IPv6 work
bridge['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
bridge['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
bridge['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
# Media Access Control (MAC) address
if conf.exists('mac'):
bridge['mac'] = conf.return_value('mac')
# Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses
# before re-adding them
if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge')
and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ):
bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix']
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']:
+ bridge['ipv6_accept_ra'] = 2
+
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
bridge['max_age'] = int(conf.return_value('max-age'))
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
# defaults are stored in util.py (they can't be here as all interface
# scripts use the function)
memberconf = get_bridge_member_config(conf, bridge['intf'], intf)
if memberconf:
memberconf['name'] = intf
bridge['member'].append(memberconf)
# Determine bridge member interface (currently effective) - to determine which
# interfaces is no longer assigend to the bridge and thus can be removed
eff_intf = conf.list_effective_nodes('member interface')
act_intf = conf.list_nodes('member interface')
bridge['member_remove'] = list_diff(eff_intf, act_intf)
# Priority for this bridge
if conf.exists('priority'):
bridge['priority'] = int(conf.return_value('priority'))
# Enable spanning tree protocol
if conf.exists('stp'):
bridge['stp'] = 1
# retrieve VRF instance
if conf.exists('vrf'):
bridge['vrf'] = conf.return_value('vrf')
return bridge
def verify(bridge):
if bridge['dhcpv6_prm_only'] and bridge['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
vrf_name = bridge['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
conf = Config()
for intf in bridge['member']:
# the interface must exist prior adding it to a bridge
if intf['name'] not in interfaces():
raise ConfigError((
f'Cannot add nonexistent interface "{intf["name"]}" '
f'to bridge "{bridge["intf"]}"'))
if intf['name'] == 'lo':
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
# bridge members aren't allowed to be members of another bridge
for br in conf.list_nodes('interfaces bridge'):
# it makes no sense to verify ourself in this case
if br == bridge['intf']:
continue
tmp = conf.list_nodes(f'interfaces bridge {br} member interface')
if intf['name'] in tmp:
raise ConfigError((
f'Cannot add interface "{intf["name"]}" to bridge '
f'"{bridge["intf"]}", it is already a member of bridge "{br}"!'))
# bridge members are not allowed to be bond members
tmp = is_member(conf, intf['name'], 'bonding')
if tmp:
raise ConfigError((
f'Cannot add interface "{intf["name"]}" to bridge '
f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!'))
# bridge members must not have an assigned address
if has_address_configured(conf, intf['name']):
raise ConfigError((
f'Cannot add interface "{intf["name"]}" to bridge '
f'"{bridge["intf"]}", it has an address assigned!'))
return None
def generate(bridge):
return None
def apply(bridge):
br = BridgeIf(bridge['intf'])
if bridge['deleted']:
# delete interface
br.remove()
else:
# enable interface
br.set_admin_state('up')
# set ageing time
br.set_ageing_time(bridge['aging'])
# set bridge forward delay
br.set_forward_delay(bridge['forwarding_delay'])
# set hello time
br.set_hello_time(bridge['hello_time'])
# configure ARP filter configuration
br.set_arp_filter(bridge['ip_disable_arp_filter'])
# configure ARP accept
br.set_arp_accept(bridge['ip_enable_arp_accept'])
# configure ARP announce
br.set_arp_announce(bridge['ip_enable_arp_announce'])
# configure ARP ignore
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ br.set_ipv6_accept_ra(bridge['ipv6_accept_ra'])
# IPv6 address autoconfiguration
br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
# IPv6 forwarding
br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
br.set_ipv6_dad_messages(bridge['ipv6_dup_addr_detect'])
# set max message age
br.set_max_age(bridge['max_age'])
# set bridge priority
br.set_priority(bridge['priority'])
# turn stp on/off
br.set_stp(bridge['stp'])
# enable or disable IGMP querier
br.set_multicast_querier(bridge['igmp_querier'])
# update interface description used e.g. within SNMP
br.set_alias(bridge['description'])
if bridge['dhcp_client_id']:
br.dhcp.v4.options['client_id'] = bridge['dhcp_client_id']
if bridge['dhcp_hostname']:
br.dhcp.v4.options['hostname'] = bridge['dhcp_hostname']
if bridge['dhcp_vendor_class_id']:
br.dhcp.v4.options['vendor_class_id'] = bridge['dhcp_vendor_class_id']
if bridge['dhcpv6_prm_only']:
br.dhcp.v6.options['dhcpv6_prm_only'] = True
if bridge['dhcpv6_temporary']:
br.dhcp.v6.options['dhcpv6_temporary'] = True
# assign/remove VRF
br.set_vrf(bridge['vrf'])
# Delete old IPv6 EUI64 addresses before changing MAC
# (adding members to a fresh bridge changes its MAC too)
for addr in bridge['ipv6_eui64_prefix_remove']:
br.del_ipv6_eui64_address(addr)
# remove interface from bridge
for intf in bridge['member_remove']:
br.del_port(intf)
# add interfaces to bridge
for member in bridge['member']:
# if we've come here we already verified the interface doesn't
# have addresses configured so just flush any remaining ones
cmd(f'ip addr flush dev "{member["name"]}"')
br.add_port(member['name'])
# Change interface MAC address
if bridge['mac']:
br.set_mac(bridge['mac'])
# Add IPv6 EUI-based addresses (must be done after adding the
# 1st bridge member or setting its MAC)
for addr in bridge['ipv6_eui64_prefix']:
br.add_ipv6_eui64_address(addr)
# up/down interface
if bridge['disable']:
br.set_admin_state('down')
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in bridge['address_remove']:
br.del_addr(addr)
for addr in bridge['address']:
br.add_addr(addr)
STPBridgeIf = STP.enable(BridgeIf)
# configure additional bridge member options
for member in bridge['member']:
i = STPBridgeIf(member['name'])
# configure ARP cache timeout
i.set_arp_cache_tmo(member['arp_cache_tmo'])
# ignore link state changes
i.set_link_detect(member['disable_link_detect'])
# set bridge port path cost
i.set_path_cost(member['cost'])
# set bridge port path priority
i.set_path_priority(member['priority'])
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/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 955022042..5f7b0014e 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -1,322 +1,325 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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 sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.ifconfig import EthernetIf
from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.validate import is_member
from vyos.config import Config
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
'description': '',
'deleted': False,
'dhcp_client_id': '',
'dhcp_hostname': '',
'dhcp_vendor_class_id': '',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
'disable': False,
'disable_link_detect': 1,
'duplex': 'auto',
'flow_control': 'on',
'hw_id': '',
'ip_arp_cache_tmo': 30,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
'is_bond_member': False,
'intf': '',
'mac': '',
'mtu': 1500,
'offload_gro': 'off',
'offload_gso': 'off',
'offload_sg': 'off',
'offload_tso': 'off',
'offload_ufo': 'off',
'speed': 'auto',
'vif_s': {},
'vif_s_remove': [],
'vif': {},
'vif_remove': [],
'vrf': ''
}
def get_config():
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
ifname = os.environ['VYOS_TAGNODE_VALUE']
conf = Config()
# check if ethernet interface has been removed
cfg_base = ['interfaces', 'ethernet', ifname]
if not conf.exists(cfg_base):
eth = deepcopy(default_config_data)
eth['intf'] = ifname
eth['deleted'] = True
# we can not bail out early as ethernet interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
# Thus we need to remove individual settings
return eth
# set new configuration level
conf.set_level(cfg_base)
eth, disabled = intf_to_dict(conf, default_config_data)
# disable ethernet flow control (pause frames)
if conf.exists('disable-flow-control'):
eth['flow_control'] = 'off'
# retrieve real hardware address
if conf.exists('hw-id'):
eth['hw_id'] = conf.return_value('hw-id')
# interface duplex
if conf.exists('duplex'):
eth['duplex'] = conf.return_value('duplex')
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
# check if we are a member of any bond
eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding')
# GRO (generic receive offload)
if conf.exists('offload-options generic-receive'):
eth['offload_gro'] = conf.return_value('offload-options generic-receive')
# GSO (generic segmentation offload)
if conf.exists('offload-options generic-segmentation'):
eth['offload_gso'] = conf.return_value('offload-options generic-segmentation')
# scatter-gather option
if conf.exists('offload-options scatter-gather'):
eth['offload_sg'] = conf.return_value('offload-options scatter-gather')
# TSO (TCP segmentation offloading)
if conf.exists('offload-options tcp-segmentation'):
eth['offload_tso'] = conf.return_value('offload-options tcp-segmentation')
# UDP fragmentation offloading
if conf.exists('offload-options udp-fragmentation'):
eth['offload_ufo'] = conf.return_value('offload-options udp-fragmentation')
# interface speed
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
# remove default IPv6 link-local address if member of a bond
if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']:
eth['ipv6_eui64_prefix'].remove('fe80::/64')
eth['ipv6_eui64_prefix_remove'].append('fe80::/64')
add_to_dict(conf, disabled, eth, 'vif', 'vif')
add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s')
return eth
def verify(eth):
if eth['deleted']:
return None
if eth['intf'] not in interfaces():
raise ConfigError(f"Interface ethernet {eth['intf']} does not exist")
if eth['speed'] == 'auto':
if eth['duplex'] != 'auto':
raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too')
if eth['duplex'] == 'auto':
if eth['speed'] != 'auto':
raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too')
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member']
if ( memberof
and ( eth['address']
or eth['ipv6_eui64_prefix']
or eth['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{eth["intf"]}" '
f'as it is a member of "{memberof}"!'))
if eth['vrf']:
if eth['vrf'] not in interfaces():
raise ConfigError(f'VRF "{eth["vrf"]}" does not exist')
if memberof:
raise ConfigError((
f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" '
f'and "{memberof}" at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(eth)
return None
def generate(eth):
return None
def apply(eth):
e = EthernetIf(eth['intf'])
if eth['deleted']:
# delete interface
e.remove()
else:
# update interface description used e.g. within SNMP
e.set_alias(eth['description'])
if eth['dhcp_client_id']:
e.dhcp.v4.options['client_id'] = eth['dhcp_client_id']
if eth['dhcp_hostname']:
e.dhcp.v4.options['hostname'] = eth['dhcp_hostname']
if eth['dhcp_vendor_class_id']:
e.dhcp.v4.options['vendor_class_id'] = eth['dhcp_vendor_class_id']
if eth['dhcpv6_prm_only']:
e.dhcp.v6.options['dhcpv6_prm_only'] = True
if eth['dhcpv6_temporary']:
e.dhcp.v6.options['dhcpv6_temporary'] = True
# ignore link state changes
e.set_link_detect(eth['disable_link_detect'])
# disable ethernet flow control (pause frames)
e.set_flow_control(eth['flow_control'])
# configure ARP cache timeout in milliseconds
e.set_arp_cache_tmo(eth['ip_arp_cache_tmo'])
# configure ARP filter configuration
e.set_arp_filter(eth['ip_disable_arp_filter'])
# configure ARP accept
e.set_arp_accept(eth['ip_enable_arp_accept'])
# configure ARP announce
e.set_arp_announce(eth['ip_enable_arp_announce'])
# configure ARP ignore
e.set_arp_ignore(eth['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
e.set_proxy_arp(eth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ e.set_ipv6_accept_ra(eth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
e.set_ipv6_autoconf(eth['ipv6_autoconf'])
# IPv6 forwarding
e.set_ipv6_forwarding(eth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect'])
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in eth['ipv6_eui64_prefix_remove']:
e.del_ipv6_eui64_address(addr)
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if eth['mac']:
e.set_mac(eth['mac'])
elif eth['hw_id']:
e.set_mac(eth['hw_id'])
# Add IPv6 EUI-based addresses
for addr in eth['ipv6_eui64_prefix']:
e.add_ipv6_eui64_address(addr)
# Maximum Transmission Unit (MTU)
e.set_mtu(eth['mtu'])
# GRO (generic receive offload)
e.set_gro(eth['offload_gro'])
# GSO (generic segmentation offload)
e.set_gso(eth['offload_gso'])
# scatter-gather option
e.set_sg(eth['offload_sg'])
# TSO (TCP segmentation offloading)
e.set_tso(eth['offload_tso'])
# UDP fragmentation offloading
e.set_ufo(eth['offload_ufo'])
# Set physical interface speed and duplex
e.set_speed_duplex(eth['speed'], eth['duplex'])
# Enable/Disable interface
if eth['disable']:
e.set_admin_state('down')
else:
e.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in eth['address_remove']:
e.del_addr(addr)
for addr in eth['address']:
e.add_addr(addr)
# assign/remove VRF (ONLY when not a member of a bridge or bond,
# otherwise 'nomaster' removes it from it)
if not ( eth['is_bridge_member'] or eth['is_bond_member'] ):
e.set_vrf(eth['vrf'])
# re-add ourselves to any bridge we might have fallen out of
if eth['is_bridge_member']:
e.add_to_bridge(eth['is_bridge_member'])
# apply all vlans to interface
apply_all_vlans(e, eth)
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/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 26bb537e5..cdfc6ea84 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -1,282 +1,290 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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 sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
from vyos.util import call
from vyos.validate import is_member, is_addr_assigned
default_config_data = {
'address': [],
'deleted': False,
'description': '',
'disable': False,
'encapsulation': 'udp',
'local_address': '',
'local_port': 5000,
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
'mtu': 1488,
'peer_session_id': '',
'peer_tunnel_id': '',
'remote_address': '',
'remote_port': 5000,
'session_id': '',
'tunnel_id': ''
}
def check_kmod():
modules = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
for module in modules:
if not os.path.exists(f'/sys/module/{module}'):
if call(f'modprobe {module}') != 0:
raise ConfigError(f'Loading Kernel module {module} failed')
def get_config():
l2tpv3 = 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')
l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if interface is member of a bridge
l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge')
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
interface = l2tpv3['intf']
# to delete the l2tpv3 interface we need the current tunnel_id and session_id
if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'):
l2tpv3['tunnel_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} tunnel-id')
if conf.exists_effective(f'interfaces l2tpv3 {interface} session-id'):
l2tpv3['session_id'] = conf.return_effective_value(f'interfaces l2tpv3 {interface} session-id')
return l2tpv3
# set new configuration level
conf.set_level('interfaces l2tpv3 ' + l2tpv3['intf'])
# retrieve configured interface addresses
if conf.exists('address'):
l2tpv3['address'] = conf.return_values('address')
# retrieve interface description
if conf.exists('description'):
l2tpv3['description'] = conf.return_value('description')
# get tunnel destination port
if conf.exists('destination-port'):
l2tpv3['remote_port'] = int(conf.return_value('destination-port'))
# Disable this interface
if conf.exists('disable'):
l2tpv3['disable'] = True
# get tunnel encapsulation type
if conf.exists('encapsulation'):
l2tpv3['encapsulation'] = conf.return_value('encapsulation')
# get tunnel local ip address
if conf.exists('local-ip'):
l2tpv3['local_address'] = conf.return_value('local-ip')
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
l2tpv3['ipv6_autoconf'] = 1
# Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Remove the default link-local address if set.
if not ( conf.exists('ipv6 address no-default-link-local') or
l2tpv3['is_bridge_member'] ):
# add the link-local by default to make IPv6 work
l2tpv3['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
l2tpv3['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if l2tpv3['ipv6_autoconf'] or 'dhcpv6' in l2tpv3['address']:
+ l2tpv3['ipv6_accept_ra'] = 2
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
l2tpv3['mtu'] = int(conf.return_value('mtu'))
# Remote session id
if conf.exists('peer-session-id'):
l2tpv3['peer_session_id'] = conf.return_value('peer-session-id')
# Remote tunnel id
if conf.exists('peer-tunnel-id'):
l2tpv3['peer_tunnel_id'] = conf.return_value('peer-tunnel-id')
# Remote address of L2TPv3 tunnel
if conf.exists('remote-ip'):
l2tpv3['remote_address'] = conf.return_value('remote-ip')
# Local session id
if conf.exists('session-id'):
l2tpv3['session_id'] = conf.return_value('session-id')
# get local tunnel port
if conf.exists('source-port'):
l2tpv3['local_port'] = conf.return_value('source-port')
# get local tunnel id
if conf.exists('tunnel-id'):
l2tpv3['tunnel_id'] = conf.return_value('tunnel-id')
return l2tpv3
def verify(l2tpv3):
interface = l2tpv3['intf']
if l2tpv3['deleted']:
if l2tpv3['is_bridge_member']:
raise ConfigError((
f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a '
f'member of bridge "{l2tpv3["is_bridge_member"]}"!'))
return None
if not l2tpv3['local_address']:
raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')
if not is_addr_assigned(l2tpv3['local_address']):
raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}')
if not l2tpv3['remote_address']:
raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')
if not l2tpv3['tunnel_id']:
raise ConfigError(f'Must configure the l2tpv3 tunnel-id for {interface}')
if not l2tpv3['peer_tunnel_id']:
raise ConfigError(f'Must configure the l2tpv3 peer-tunnel-id for {interface}')
if not l2tpv3['session_id']:
raise ConfigError(f'Must configure the l2tpv3 session-id for {interface}')
if not l2tpv3['peer_session_id']:
raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
if ( l2tpv3['is_bridge_member']
and ( l2tpv3['address']
or l2tpv3['ipv6_eui64_prefix']
or l2tpv3['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{l2tpv3["intf"]}" '
f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!'))
return None
def generate(l2tpv3):
return None
def apply(l2tpv3):
# L2TPv3 interface needs to be created/deleted on-block, instead of
# passing a ton of arguments, I just use a dict that is managed by
# vyos.ifconfig
conf = deepcopy(L2TPv3If.get_config())
# Check if L2TPv3 interface already exists
if l2tpv3['intf'] in interfaces():
# L2TPv3 is picky when changing tunnels/sessions, thus we can simply
# always delete it first.
conf['session_id'] = l2tpv3['session_id']
conf['tunnel_id'] = l2tpv3['tunnel_id']
l = L2TPv3If(l2tpv3['intf'], **conf)
l.remove()
if not l2tpv3['deleted']:
conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id']
conf['local_port'] = l2tpv3['local_port']
conf['remote_port'] = l2tpv3['remote_port']
conf['encapsulation'] = l2tpv3['encapsulation']
conf['local_address'] = l2tpv3['local_address']
conf['remote_address'] = l2tpv3['remote_address']
conf['session_id'] = l2tpv3['session_id']
conf['tunnel_id'] = l2tpv3['tunnel_id']
conf['peer_session_id'] = l2tpv3['peer_session_id']
# Finally create the new interface
l = L2TPv3If(l2tpv3['intf'], **conf)
# update interface description used e.g. by SNMP
l.set_alias(l2tpv3['description'])
# Maximum Transfer Unit (MTU)
l.set_mtu(l2tpv3['mtu'])
+ # IPv6 accept RA
+ l.set_ipv6_accept_ra(l2tpv3['ipv6_accept_ra'])
# IPv6 address autoconfiguration
l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
# IPv6 forwarding
l.set_ipv6_forwarding(l2tpv3['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
l.set_ipv6_dad_messages(l2tpv3['ipv6_dup_addr_detect'])
# Configure interface address(es) - no need to implicitly delete the
# old addresses as they have already been removed by deleting the
# interface above
for addr in l2tpv3['address']:
l.add_addr(addr)
# IPv6 EUI-based addresses
for addr in l2tpv3['ipv6_eui64_prefix']:
l.add_ipv6_eui64_address(addr)
# As the interface is always disabled first when changing parameters
# we will only re-enable the interface if it is not administratively
# disabled
if not l2tpv3['disable']:
l.set_admin_state('up')
# re-add ourselves to any bridge we might have fallen out of
if l2tpv3['is_bridge_member']:
l.add_to_bridge(l2tpv3['is_bridge_member'])
return None
if __name__ == '__main__':
try:
check_kmod()
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index bd69e4d4b..ea8e1a7c4 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,1084 +1,1092 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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
import re
from copy import deepcopy
from sys import exit,stderr
from ipaddress import ip_address,ip_network,IPv4Address,IPv4Network,IPv6Address,IPv6Network,summarize_address_range
from netifaces import interfaces
from time import sleep
from shutil import rmtree
from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import VTunIf
from vyos.template import render
from vyos.util import call, chown, chmod_600, chmod_755
from vyos.validate import is_addr_assigned, is_member, is_ipv4
from vyos import ConfigError
user = 'openvpn'
group = 'openvpn'
default_config_data = {
'address': [],
'auth_user': '',
'auth_pass': '',
'auth_user_pass_file': '',
'auth': False,
'compress_lzo': False,
'deleted': False,
'description': '',
'disable': False,
'disable_ncp': False,
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'ipv6_local_address': [],
'ipv6_remote_address': [],
'is_bridge_member': False,
'ping_restart': '60',
'ping_interval': '10',
'local_address': [],
'local_address_subnet': '',
'local_host': '',
'local_port': '',
'mode': '',
'ncp_ciphers': '',
'options': [],
'persistent_tunnel': False,
'protocol': 'udp',
'protocol_real': '',
'redirect_gateway': '',
'remote_address': [],
'remote_host': [],
'remote_port': '',
'client': [],
'server_domain': '',
'server_max_conn': '',
'server_dns_nameserver': [],
'server_pool': True,
'server_pool_start': '',
'server_pool_stop': '',
'server_pool_netmask': '',
'server_push_route': [],
'server_reject_unconfigured': False,
'server_subnet': [],
'server_topology': '',
'server_ipv6_dns_nameserver': [],
'server_ipv6_local': '',
'server_ipv6_prefixlen': '',
'server_ipv6_remote': '',
'server_ipv6_pool': True,
'server_ipv6_pool_base': '',
'server_ipv6_pool_prefixlen': '',
'server_ipv6_push_route': [],
'server_ipv6_subnet': [],
'shared_secret_file': '',
'tls': False,
'tls_auth': '',
'tls_ca_cert': '',
'tls_cert': '',
'tls_crl': '',
'tls_dh': '',
'tls_key': '',
'tls_crypt': '',
'tls_role': '',
'tls_version_min': '',
'type': 'tun',
'uid': user,
'gid': group,
}
def get_config_name(intf):
cfg_file = f'/run/openvpn/{intf}.conf'
return cfg_file
def checkCertHeader(header, filename):
"""
Verify if filename contains specified header.
Returns True if match is found, False if no match or file is not found
"""
if not os.path.isfile(filename):
return False
with open(filename, 'r') as f:
for line in f:
if re.match(header, line):
return True
return False
def getDefaultServer(network, topology, devtype):
"""
Gets the default server parameters for a IPv4 "server" directive.
Logic from openvpn's src/openvpn/helper.c.
Returns a dict with addresses or False if the input parameters were incorrect.
"""
if not (devtype == 'tun' or devtype == 'tap'):
return False
if not network.version == 4:
return False
elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30):
return False
server = {
'local': '',
'remote_netmask': '',
'client_remote_netmask': '',
'pool_start': '',
'pool_stop': '',
'pool_netmask': ''
}
if devtype == 'tun':
if topology == 'net30' or topology == 'point-to-point':
server['local'] = network[1]
server['remote_netmask'] = network[2]
server['client_remote_netmask'] = server['local']
# pool start is 4th host IP in subnet (.4 in a /24)
server['pool_start'] = network[4]
if network.prefixlen == 29:
server['pool_stop'] = network.broadcast_address
else:
# pool end is -4 from the broadcast address (.251 in a /24)
server['pool_stop'] = network[-5]
elif topology == 'subnet':
server['local'] = network[1]
server['remote_netmask'] = str(network.netmask)
server['client_remote_netmask'] = server['remote_netmask']
server['pool_start'] = network[2]
server['pool_stop'] = network[-3]
server['pool_netmask'] = server['remote_netmask']
elif devtype == 'tap':
server['local'] = network[1]
server['remote_netmask'] = str(network.netmask)
server['client_remote_netmask'] = server['remote_netmask']
server['pool_start'] = network[2]
server['pool_stop'] = network[-2]
server['pool_netmask'] = server['remote_netmask']
return server
def get_config():
openvpn = 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')
openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
# check if interface is member of a bridge
openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
return openvpn
# bridged server should not have a pool by default (but can be specified manually)
if openvpn['is_bridge_member']:
openvpn['server_pool'] = False
openvpn['server_ipv6_pool'] = False
# set configuration level
conf.set_level('interfaces openvpn ' + openvpn['intf'])
# retrieve authentication options - username
if conf.exists('authentication username'):
openvpn['auth_user'] = conf.return_value('authentication username')
openvpn['auth'] = True
# retrieve authentication options - username
if conf.exists('authentication password'):
openvpn['auth_pass'] = conf.return_value('authentication password')
openvpn['auth'] = True
# retrieve interface description
if conf.exists('description'):
openvpn['description'] = conf.return_value('description')
# interface device-type
if conf.exists('device-type'):
openvpn['type'] = conf.return_value('device-type')
# disable interface
if conf.exists('disable'):
openvpn['disable'] = True
# data encryption algorithm cipher
if conf.exists('encryption cipher'):
openvpn['encryption'] = conf.return_value('encryption cipher')
# disable ncp-ciphers support
if conf.exists('encryption disable-ncp'):
openvpn['disable_ncp'] = True
# data encryption algorithm ncp-list
if conf.exists('encryption ncp-ciphers'):
_ncp_ciphers = []
for enc in conf.return_values('encryption ncp-ciphers'):
if enc == 'des':
_ncp_ciphers.append('des-cbc')
_ncp_ciphers.append('DES-CBC')
elif enc == '3des':
_ncp_ciphers.append('des-ede3-cbc')
_ncp_ciphers.append('DES-EDE3-CBC')
elif enc == 'aes128':
_ncp_ciphers.append('aes-128-cbc')
_ncp_ciphers.append('AES-128-CBC')
elif enc == 'aes128gcm':
_ncp_ciphers.append('aes-128-gcm')
_ncp_ciphers.append('AES-128-GCM')
elif enc == 'aes192':
_ncp_ciphers.append('aes-192-cbc')
_ncp_ciphers.append('AES-192-CBC')
elif enc == 'aes192gcm':
_ncp_ciphers.append('aes-192-gcm')
_ncp_ciphers.append('AES-192-GCM')
elif enc == 'aes256':
_ncp_ciphers.append('aes-256-cbc')
_ncp_ciphers.append('AES-256-CBC')
elif enc == 'aes256gcm':
_ncp_ciphers.append('aes-256-gcm')
_ncp_ciphers.append('AES-256-GCM')
openvpn['ncp_ciphers'] = ':'.join(_ncp_ciphers)
# hash algorithm
if conf.exists('hash'):
openvpn['hash'] = conf.return_value('hash')
# Maximum number of keepalive packet failures
if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'):
fail_count = conf.return_value('keep-alive failure-count')
interval = conf.return_value('keep-alive interval')
openvpn['ping_interval' ] = interval
openvpn['ping_restart' ] = int(interval) * int(fail_count)
# Local IP address of tunnel - even as it is a tag node - we can only work
# on the first address
if conf.exists('local-address'):
for tmp in conf.list_nodes('local-address'):
tmp_ip = ip_address(tmp)
if tmp_ip.version == 4:
openvpn['local_address'].append(tmp)
if conf.exists('local-address {} subnet-mask'.format(tmp)):
openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(tmp))
elif tmp_ip.version == 6:
# input IPv6 address could be expanded so get the compressed version
openvpn['ipv6_local_address'].append(str(tmp_ip))
# Local IP address to accept connections
if conf.exists('local-host'):
openvpn['local_host'] = conf.return_value('local-host')
# Local port number to accept connections
if conf.exists('local-port'):
openvpn['local_port'] = conf.return_value('local-port')
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
openvpn['ipv6_autoconf'] = 1
# Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Determine currently effective EUI64 addresses - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values('ipv6 address eui64')
openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
# Remove the default link-local address if set.
if conf.exists('ipv6 address no-default-link-local'):
openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
else:
# add the link-local by default to make IPv6 work
openvpn['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
openvpn['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
+ openvpn['ipv6_accept_ra'] = 2
+
# OpenVPN operation mode
if conf.exists('mode'):
openvpn['mode'] = conf.return_value('mode')
# Additional OpenVPN options
if conf.exists('openvpn-option'):
openvpn['options'] = conf.return_values('openvpn-option')
# Do not close and reopen interface
if conf.exists('persistent-tunnel'):
openvpn['persistent_tunnel'] = True
# Communication protocol
if conf.exists('protocol'):
openvpn['protocol'] = conf.return_value('protocol')
# IP address of remote end of tunnel
if conf.exists('remote-address'):
for tmp in conf.return_values('remote-address'):
tmp_ip = ip_address(tmp)
if tmp_ip.version == 4:
openvpn['remote_address'].append(tmp)
elif tmp_ip.version == 6:
openvpn['ipv6_remote_address'].append(str(tmp_ip))
# Remote host to connect to (dynamic if not set)
if conf.exists('remote-host'):
openvpn['remote_host'] = conf.return_values('remote-host')
# Remote port number to connect to
if conf.exists('remote-port'):
openvpn['remote_port'] = conf.return_value('remote-port')
# OpenVPN tunnel to be used as the default route
# see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/
# redirect-gateway flags
if conf.exists('replace-default-route'):
openvpn['redirect_gateway'] = 'def1'
if conf.exists('replace-default-route local'):
openvpn['redirect_gateway'] = 'local def1'
# Topology for clients
if conf.exists('server topology'):
openvpn['server_topology'] = conf.return_value('server topology')
# Server-mode subnet (from which client IPs are allocated)
server_network_v4 = None
server_network_v6 = None
if conf.exists('server subnet'):
for tmp in conf.return_values('server subnet'):
tmp_ip = ip_network(tmp)
if tmp_ip.version == 4:
server_network_v4 = tmp_ip
# convert the network to format: "192.0.2.0 255.255.255.0" for later use in template
openvpn['server_subnet'].append(tmp_ip.with_netmask.replace(r'/', ' '))
elif tmp_ip.version == 6:
server_network_v6 = tmp_ip
openvpn['server_ipv6_subnet'].append(str(tmp_ip))
# Client-specific settings
for client in conf.list_nodes('server client'):
# set configuration level
conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client)
data = {
'name': client,
'disable': False,
'ip': [],
'ipv6_ip': [],
'ipv6_remote': '',
'ipv6_push_route': [],
'ipv6_subnet': [],
'push_route': [],
'subnet': [],
'remote_netmask': ''
}
# Option to disable client connection
if conf.exists('disable'):
data['disable'] = True
# IP address of the client
for tmp in conf.return_values('ip'):
tmp_ip = ip_address(tmp)
if tmp_ip.version == 4:
data['ip'].append(tmp)
elif tmp_ip.version == 6:
data['ipv6_ip'].append(str(tmp_ip))
# Route to be pushed to the client
for tmp in conf.return_values('push-route'):
tmp_ip = ip_network(tmp)
if tmp_ip.version == 4:
data['push_route'].append(tmp_ip.with_netmask.replace(r'/', ' '))
elif tmp_ip.version == 6:
data['ipv6_push_route'].append(str(tmp_ip))
# Subnet belonging to the client
for tmp in conf.return_values('subnet'):
tmp_ip = ip_network(tmp)
if tmp_ip.version == 4:
data['subnet'].append(tmp_ip.with_netmask.replace(r'/', ' '))
elif tmp_ip.version == 6:
data['ipv6_subnet'].append(str(tmp_ip))
# Append to global client list
openvpn['client'].append(data)
# re-set configuration level
conf.set_level('interfaces openvpn ' + openvpn['intf'])
# Server client IP pool
if conf.exists('server client-ip-pool'):
conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ip-pool')
# enable or disable server_pool where necessary
# default is enabled, or disabled in bridge mode
openvpn['server_pool'] = not conf.exists('disable')
if conf.exists('start'):
openvpn['server_pool_start'] = conf.return_value('start')
if conf.exists('stop'):
openvpn['server_pool_stop'] = conf.return_value('stop')
if conf.exists('netmask'):
openvpn['server_pool_netmask'] = conf.return_value('netmask')
conf.set_level('interfaces openvpn ' + openvpn['intf'])
# Server client IPv6 pool
if conf.exists('server client-ipv6-pool'):
conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ipv6-pool')
openvpn['server_ipv6_pool'] = not conf.exists('disable')
if conf.exists('base'):
tmp = conf.return_value('base').split('/')
openvpn['server_ipv6_pool_base'] = str(IPv6Address(tmp[0]))
if 1 < len(tmp):
openvpn['server_ipv6_pool_prefixlen'] = tmp[1]
conf.set_level('interfaces openvpn ' + openvpn['intf'])
# DNS suffix to be pushed to all clients
if conf.exists('server domain-name'):
openvpn['server_domain'] = conf.return_value('server domain-name')
# Number of maximum client connections
if conf.exists('server max-connections'):
openvpn['server_max_conn'] = conf.return_value('server max-connections')
# Domain Name Server (DNS)
if conf.exists('server name-server'):
for tmp in conf.return_values('server name-server'):
tmp_ip = ip_address(tmp)
if tmp_ip.version == 4:
openvpn['server_dns_nameserver'].append(tmp)
elif tmp_ip.version == 6:
openvpn['server_ipv6_dns_nameserver'].append(str(tmp_ip))
# Route to be pushed to all clients
if conf.exists('server push-route'):
for tmp in conf.return_values('server push-route'):
tmp_ip = ip_network(tmp)
if tmp_ip.version == 4:
openvpn['server_push_route'].append(tmp_ip.with_netmask.replace(r'/', ' '))
elif tmp_ip.version == 6:
openvpn['server_ipv6_push_route'].append(str(tmp_ip))
# Reject connections from clients that are not explicitly configured
if conf.exists('server reject-unconfigured-clients'):
openvpn['server_reject_unconfigured'] = True
# File containing TLS auth static key
if conf.exists('tls auth-file'):
openvpn['tls_auth'] = conf.return_value('tls auth-file')
openvpn['tls'] = True
# File containing certificate for Certificate Authority (CA)
if conf.exists('tls ca-cert-file'):
openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file')
openvpn['tls'] = True
# File containing certificate for this host
if conf.exists('tls cert-file'):
openvpn['tls_cert'] = conf.return_value('tls cert-file')
openvpn['tls'] = True
# File containing certificate revocation list (CRL) for this host
if conf.exists('tls crl-file'):
openvpn['tls_crl'] = conf.return_value('tls crl-file')
openvpn['tls'] = True
# File containing Diffie Hellman parameters (server only)
if conf.exists('tls dh-file'):
openvpn['tls_dh'] = conf.return_value('tls dh-file')
openvpn['tls'] = True
# File containing this host's private key
if conf.exists('tls key-file'):
openvpn['tls_key'] = conf.return_value('tls key-file')
openvpn['tls'] = True
# File containing key to encrypt control channel packets
if conf.exists('tls crypt-file'):
openvpn['tls_crypt'] = conf.return_value('tls crypt-file')
openvpn['tls'] = True
# Role in TLS negotiation
if conf.exists('tls role'):
openvpn['tls_role'] = conf.return_value('tls role')
openvpn['tls'] = True
# Minimum required TLS version
if conf.exists('tls tls-version-min'):
openvpn['tls_version_min'] = conf.return_value('tls tls-version-min')
openvpn['tls'] = True
if conf.exists('shared-secret-key-file'):
openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file')
if conf.exists('use-lzo-compression'):
openvpn['compress_lzo'] = True
# Special case when using EC certificates:
# if key-file is EC and dh-file is unset, set tls_dh to 'none'
if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
openvpn['tls_dh'] = 'none'
# set default server topology to net30
if openvpn['mode'] == 'server' and not openvpn['server_topology']:
openvpn['server_topology'] = 'net30'
# Convert protocol to real protocol used by openvpn.
# To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols
# (https://community.openvpn.net/openvpn/ticket/360), unless local is IPv4
# in which case it must use the standard protocols.
# Note: this will break openvpn if IPv6 is disabled on the system.
# This currently isn't supported, a check can be added in the future.
if openvpn['protocol'] == 'tcp-active':
openvpn['protocol_real'] = 'tcp6-client'
elif openvpn['protocol'] == 'tcp-passive':
openvpn['protocol_real'] = 'tcp6-server'
else:
openvpn['protocol_real'] = 'udp6'
if is_ipv4(openvpn['local_host']):
# takes out the '6'
openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:]
# Set defaults where necessary.
# If any of the input parameters are wrong,
# this will return False and no defaults will be set.
if server_network_v4 and openvpn['server_topology'] and openvpn['type']:
default_server = None
default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type'])
if default_server:
# server-bridge doesn't require a pool so don't set defaults for it
if openvpn['server_pool'] and not openvpn['is_bridge_member']:
if not openvpn['server_pool_start']:
openvpn['server_pool_start'] = default_server['pool_start']
if not openvpn['server_pool_stop']:
openvpn['server_pool_stop'] = default_server['pool_stop']
if not openvpn['server_pool_netmask']:
openvpn['server_pool_netmask'] = default_server['pool_netmask']
for client in openvpn['client']:
client['remote_netmask'] = default_server['client_remote_netmask']
if server_network_v6:
if not openvpn['server_ipv6_local']:
openvpn['server_ipv6_local'] = server_network_v6[1]
if not openvpn['server_ipv6_prefixlen']:
openvpn['server_ipv6_prefixlen'] = server_network_v6.prefixlen
if not openvpn['server_ipv6_remote']:
openvpn['server_ipv6_remote'] = server_network_v6[2]
if openvpn['server_ipv6_pool'] and server_network_v6.prefixlen < 112:
if not openvpn['server_ipv6_pool_base']:
openvpn['server_ipv6_pool_base'] = server_network_v6[0x1000]
if not openvpn['server_ipv6_pool_prefixlen']:
openvpn['server_ipv6_pool_prefixlen'] = openvpn['server_ipv6_prefixlen']
for client in openvpn['client']:
client['ipv6_remote'] = openvpn['server_ipv6_local']
if openvpn['redirect_gateway']:
openvpn['redirect_gateway'] += ' ipv6'
return openvpn
def verify(openvpn):
if openvpn['deleted']:
if openvpn['is_bridge_member']:
raise ConfigError((
f'Cannot delete interface "{openvpn["intf"]}" as it is a '
f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
# Check if we have disabled ncp and at the same time specified ncp-ciphers
if openvpn['disable_ncp'] and openvpn['ncp_ciphers']:
raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"')
#
# OpenVPN client mode - VERIFY
#
if openvpn['mode'] == 'client':
if openvpn['local_port']:
raise ConfigError('Cannot specify "local-port" in client mode')
if openvpn['local_host']:
raise ConfigError('Cannot specify "local-host" in client mode')
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
if not openvpn['remote_host']:
raise ConfigError('Must specify "remote-host" in client mode')
if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" in client mode')
#
# OpenVPN site-to-site - VERIFY
#
if openvpn['mode'] == 'site-to-site':
if openvpn['ncp_ciphers']:
raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client')
if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
raise ConfigError('Must specify "local-address" or add interface to bridge')
if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
if len(openvpn['remote_address']) > 1 or len(openvpn['ipv6_remote_address']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "remote-address"')
for host in openvpn['remote_host']:
if host in openvpn['remote_address'] or host in openvpn['ipv6_remote_address']:
raise ConfigError('"remote-address" cannot be the same as "remote-host"')
if openvpn['local_address'] and not (openvpn['remote_address'] or openvpn['local_address_subnet']):
raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address" or IPv4 "local-address subnet"')
if openvpn['remote_address'] and not openvpn['local_address']:
raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
if openvpn['ipv6_local_address'] and not openvpn['ipv6_remote_address']:
raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
if openvpn['ipv6_remote_address'] and not openvpn['ipv6_local_address']:
raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
if openvpn['type'] == 'tun':
if not (openvpn['remote_address'] or openvpn['ipv6_remote_address']):
raise ConfigError('Must specify "remote-address"')
if ( (openvpn['local_address'] and openvpn['local_address'] == openvpn['remote_address']) or
(openvpn['ipv6_local_address'] and openvpn['ipv6_local_address'] == openvpn['ipv6_remote_address']) ):
raise ConfigError('"local-address" and "remote-address" cannot be the same')
if openvpn['local_host'] in openvpn['local_address'] or openvpn['local_host'] in openvpn['ipv6_local_address']:
raise ConfigError('"local-address" cannot be the same as "local-host"')
else:
# checks for client-server or site-to-site bridged
if openvpn['local_address'] or openvpn['ipv6_local_address'] or openvpn['remote_address'] or openvpn['ipv6_remote_address']:
raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or bridge mode')
#
# OpenVPN server mode - VERIFY
#
if openvpn['mode'] == 'server':
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Protocol "tcp-active" is not valid in server mode')
if openvpn['remote_port']:
raise ConfigError('Cannot specify "remote-port" in server mode')
if openvpn['remote_host']:
raise ConfigError('Cannot specify "remote-host" in server mode')
if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1:
raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"')
if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
if len(openvpn['server_subnet']) > 1 or len(openvpn['server_ipv6_subnet']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 server subnet')
for client in openvpn['client']:
if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1:
raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
if openvpn['server_subnet']:
subnet = IPv4Network(openvpn['server_subnet'][0].replace(' ', '/'))
if openvpn['type'] == 'tun' and subnet.prefixlen > 29:
raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
elif openvpn['type'] == 'tap' and subnet.prefixlen > 30:
raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
for client in openvpn['client']:
if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
else:
if not openvpn['is_bridge_member']:
raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
if openvpn['server_pool']:
if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode')
else:
v4PoolStart = IPv4Address(openvpn['server_pool_start'])
v4PoolStop = IPv4Address(openvpn['server_pool_stop'])
if v4PoolStart > v4PoolStop:
raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
if v4PoolSize >= 65536:
raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
for client in openvpn['client']:
if client['ip']:
for v4PoolNet in v4PoolNets:
if IPv4Address(client['ip'][0]) in v4PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.',
file=stderr)
if openvpn['server_ipv6_subnet']:
if not openvpn['server_subnet']:
raise ConfigError('IPv6 server requires an IPv4 server subnet')
if openvpn['server_ipv6_pool']:
if not openvpn['server_pool']:
raise ConfigError('IPv6 server pool requires an IPv4 server pool')
if int(openvpn['server_ipv6_pool_prefixlen']) >= 112:
raise ConfigError('IPv6 server pool must be larger than /112')
v6PoolStart = IPv6Address(openvpn['server_ipv6_pool_base'])
v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
if v6PoolSize < v4PoolSize:
raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
for client in openvpn['client']:
if client['ipv6_ip']:
for v6PoolNet in v6PoolNets:
if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.',
file=stderr)
else:
if openvpn['server_ipv6_push_route']:
raise ConfigError('IPv6 push-route requires an IPv6 server subnet')
for client in openvpn ['client']:
if client['ipv6_ip']:
raise ConfigError(f'Server client "{client["name"]}" IPv6 IP requires an IPv6 server subnet')
if client['ipv6_push_route']:
raise ConfigError(f'Server client "{client["name"]} IPv6 push-route requires an IPv6 server subnet"')
if client['ipv6_subnet']:
raise ConfigError(f'Server client "{client["name"]} IPv6 subnet requires an IPv6 server subnet"')
else:
# checks for both client and site-to-site go here
if openvpn['server_reject_unconfigured']:
raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode')
if openvpn['server_topology']:
raise ConfigError('The "topology" option is only valid in server mode')
if (not openvpn['remote_host']) and openvpn['redirect_gateway']:
raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
#
# OpenVPN common verification section
# not depending on any operation mode
#
# verify specified IP address is present on any interface on this system
if openvpn['local_host']:
if not is_addr_assigned(openvpn['local_host']):
raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host']))
# TCP active
if openvpn['protocol'] == 'tcp-active':
if openvpn['local_port']:
raise ConfigError('Cannot specify "local-port" with "tcp-active"')
if not openvpn['remote_host']:
raise ConfigError('Must specify "remote-host" with "tcp-active"')
# shared secret and TLS
if not (openvpn['shared_secret_file'] or openvpn['tls']):
raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
if openvpn['shared_secret_file'] and openvpn['tls']:
raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
if openvpn['mode'] in ['client', 'server']:
if not openvpn['tls']:
raise ConfigError('Must specify "tls" in client-server mode')
#
# TLS/encryption
#
if openvpn['shared_secret_file']:
if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
raise ConfigError('GCM encryption with shared-secret-key-file is not supported')
if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']):
raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file']))
if openvpn['tls']:
if not openvpn['tls_ca_cert']:
raise ConfigError('Must specify "tls ca-cert-file"')
if not (openvpn['mode'] == 'client' and openvpn['auth']):
if not openvpn['tls_cert']:
raise ConfigError('Must specify "tls cert-file"')
if not openvpn['tls_key']:
raise ConfigError('Must specify "tls key-file"')
if openvpn['tls_auth'] and openvpn['tls_crypt']:
raise ConfigError('TLS auth and crypt are mutually exclusive')
if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']):
raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert']))
if openvpn['tls_auth']:
if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']):
raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth']))
if openvpn['tls_cert']:
if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']):
raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
if openvpn['tls_key']:
if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']):
raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key']))
if openvpn['tls_crypt']:
if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']):
raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt']))
if openvpn['tls_crl']:
if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']):
raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl']))
if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']):
raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh']))
if openvpn['tls_role']:
if openvpn['mode'] in ['client', 'server']:
if not openvpn['tls_auth']:
raise ConfigError('Cannot specify "tls role" in client-server mode')
if openvpn['tls_role'] == 'active':
if openvpn['protocol'] == 'tcp-passive':
raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
elif openvpn['tls_role'] == 'passive':
if openvpn['protocol'] == 'tcp-active':
raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
if not openvpn['tls_dh']:
raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH')
else:
print('Diffie-Hellman prime file is unspecified, assuming ECDH')
#
# Auth user/pass
#
if openvpn['auth']:
if not openvpn['auth_user']:
raise ConfigError('Username for authentication is missing')
if not openvpn['auth_pass']:
raise ConfigError('Password for authentication is missing')
return None
def generate(openvpn):
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
# we can't know in advance which clients have been removed,
# thus all client configs will be removed and re-added on demand
ccd_dir = os.path.join(directory, 'ccd', interface)
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
if openvpn['deleted'] or openvpn['disable']:
return None
# create config directory on demand
directories = []
directories.append(f'{directory}/status')
directories.append(f'{directory}/ccd/{interface}')
for onedir in directories:
if not os.path.exists(onedir):
os.makedirs(onedir, 0o755)
chown(onedir, user, group)
# Fix file permissons for keys
fix_permissions = []
fix_permissions.append(openvpn['shared_secret_file'])
fix_permissions.append(openvpn['tls_key'])
# Generate User/Password authentication file
if openvpn['auth']:
with open(openvpn['auth_user_pass_file'], 'w') as f:
f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass']))
# also change permission on auth file
fix_permissions.append(openvpn['auth_user_pass_file'])
else:
# delete old auth file if present
if os.path.isfile(openvpn['auth_user_pass_file']):
os.remove(openvpn['auth_user_pass_file'])
# Generate client specific configuration
for client in openvpn['client']:
client_file = os.path.join(ccd_dir, client['name'])
render(client_file, 'openvpn/client.conf.tmpl', client)
chown(client_file, user, group)
# we need to support quoting of raw parameters from OpenVPN CLI
# see https://phabricator.vyos.net/T1632
render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn,
formater=lambda _: _.replace(""", '"'))
chown(get_config_name(interface), user, group)
# Fixup file permissions
for file in fix_permissions:
chmod_600(file)
return None
def apply(openvpn):
interface = openvpn['intf']
call(f'systemctl stop openvpn@{interface}.service')
# Do some cleanup when OpenVPN is disabled/deleted
if openvpn['deleted'] or openvpn['disable']:
# cleanup old configuration files
cleanup = []
cleanup.append(get_config_name(interface))
cleanup.append(openvpn['auth_user_pass_file'])
for file in cleanup:
if os.path.isfile(file):
os.unlink(file)
return None
# On configuration change we need to wait for the 'old' interface to
# vanish from the Kernel, if it is not gone, OpenVPN will report:
# ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16)
while interface in interfaces():
sleep(0.250) # 250ms
# No matching OpenVPN process running - maybe it got killed or none
# existed - nevertheless, spawn new OpenVPN process
call(f'systemctl start openvpn@{interface}.service')
# better late then sorry ... but we can only set interface alias after
# OpenVPN has been launched and created the interface
cnt = 0
while interface not in interfaces():
# If VPN tunnel can't be established because the peer/server isn't
# (temporarily) available, the vtun interface never becomes registered
# with the kernel, and the commit would hang if there is no bail out
# condition
cnt += 1
if cnt == 50:
break
# sleep 250ms
sleep(0.250)
try:
# we need to catch the exception if the interface is not up due to
# reason stated above
o = VTunIf(interface)
# update interface description used e.g. within SNMP
o.set_alias(openvpn['description'])
+ # IPv6 accept RA
+ o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
# IPv6 address autoconfiguration
o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
# IPv6 forwarding
o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
# IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
# If MAC has changed, old EUI64 addresses won't get deleted,
# but this isn't easy to solve, so leave them.
# This is even more difficult as openvpn uses a random MAC for the
# initial interface creation, unless set by 'lladdr'.
# NOTE: right now the interface is always deleted. For future
# compatibility when tap's are not deleted, leave the del_ in
if openvpn['mode'] == 'tap':
for addr in openvpn['ipv6_eui64_prefix_remove']:
o.del_ipv6_eui64_address(addr)
for addr in openvpn['ipv6_eui64_prefix']:
o.add_ipv6_eui64_address(addr)
except:
pass
# TAP interface needs to be brought up explicitly
if openvpn['type'] == 'tap':
if not openvpn['disable']:
VTunIf(interface).set_admin_state('up')
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/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index ec2f1146e..a050ae80b 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -1,269 +1,272 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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, intf_to_dict, add_to_dict
from vyos.ifconfig import MACVLANIf, Section
from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
'description': '',
'deleted': False,
'dhcp_client_id': '',
'dhcp_hostname': '',
'dhcp_vendor_class_id': '',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
'disable': False,
'disable_link_detect': 1,
'intf': '',
'ip_arp_cache_tmo': 30,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
'source_interface': '',
'source_interface_changed': False,
'mac': '',
'mode': 'private',
'vif_s': {},
'vif_s_remove': [],
'vif': {},
'vif_remove': [],
'vrf': ''
}
def get_config():
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
ifname = os.environ['VYOS_TAGNODE_VALUE']
conf = Config()
# Check if interface has been removed
cfg_base = ['interfaces', 'pseudo-ethernet', ifname]
if not conf.exists(cfg_base):
peth = deepcopy(default_config_data)
peth['deleted'] = True
return peth
# set new configuration level
conf.set_level(cfg_base)
peth, disabled = intf_to_dict(conf, default_config_data)
# ARP cache entry timeout in seconds
if conf.exists(['ip', 'arp-cache-timeout']):
peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout']))
# Enable private VLAN proxy ARP on this interface
if conf.exists(['ip', 'proxy-arp-pvlan']):
peth['ip_proxy_arp_pvlan'] = 1
# Physical interface
if conf.exists(['source-interface']):
peth['source_interface'] = conf.return_value(['source-interface'])
tmp = conf.return_effective_value(['source-interface'])
if tmp != peth['source_interface']:
peth['source_interface_changed'] = True
# MACvlan mode
if conf.exists(['mode']):
peth['mode'] = conf.return_value(['mode'])
add_to_dict(conf, disabled, peth, 'vif', 'vif')
add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s')
return peth
def verify(peth):
if peth['deleted']:
if peth['is_bridge_member']:
raise ConfigError((
f'Cannot delete interface "{peth["intf"]}" as it is a '
f'member of bridge "{peth["is_bridge_member"]}"!'))
return None
if not peth['source_interface']:
raise ConfigError((
f'Link device must be set for pseudo-ethernet "{peth["intf"]}"'))
if not peth['source_interface'] in interfaces():
raise ConfigError((
f'Pseudo-ethernet "{peth["intf"]}" link device does not exist'))
if ( peth['is_bridge_member']
and ( peth['address']
or peth['ipv6_eui64_prefix']
or peth['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{peth["intf"]}" '
f'as it is a member of bridge "{peth["is_bridge_member"]}"!'))
if peth['vrf']:
if peth['vrf'] not in interfaces():
raise ConfigError(f'VRF "{peth["vrf"]}" does not exist')
if peth['is_bridge_member']:
raise ConfigError((
f'Interface "{peth["intf"]}" cannot be member of VRF '
f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} '
f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(peth)
return None
def generate(peth):
return None
def apply(peth):
if peth['deleted']:
# delete interface
MACVLANIf(peth['intf']).remove()
return None
# Check if MACVLAN interface already exists. Parameters like the underlaying
# source-interface device can not be changed on the fly and the interface
# needs to be recreated from the bottom.
if peth['intf'] in interfaces():
if peth['source_interface_changed']:
MACVLANIf(peth['intf']).remove()
# MACVLAN interface needs to be created on-block instead of passing a ton
# of arguments, I just use a dict that is managed by vyos.ifconfig
conf = deepcopy(MACVLANIf.get_config())
# Assign MACVLAN instance configuration parameters to config dict
conf['source_interface'] = peth['source_interface']
conf['mode'] = peth['mode']
# 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
p = MACVLANIf(peth['intf'], **conf)
# update interface description used e.g. within SNMP
p.set_alias(peth['description'])
if peth['dhcp_client_id']:
p.dhcp.v4.options['client_id'] = peth['dhcp_client_id']
if peth['dhcp_hostname']:
p.dhcp.v4.options['hostname'] = peth['dhcp_hostname']
if peth['dhcp_vendor_class_id']:
p.dhcp.v4.options['vendor_class_id'] = peth['dhcp_vendor_class_id']
if peth['dhcpv6_prm_only']:
p.dhcp.v6.options['dhcpv6_prm_only'] = True
if peth['dhcpv6_temporary']:
p.dhcp.v6.options['dhcpv6_temporary'] = True
# ignore link state changes
p.set_link_detect(peth['disable_link_detect'])
# configure ARP cache timeout in milliseconds
p.set_arp_cache_tmo(peth['ip_arp_cache_tmo'])
# configure ARP filter configuration
p.set_arp_filter(peth['ip_disable_arp_filter'])
# configure ARP accept
p.set_arp_accept(peth['ip_enable_arp_accept'])
# configure ARP announce
p.set_arp_announce(peth['ip_enable_arp_announce'])
# configure ARP ignore
p.set_arp_ignore(peth['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
p.set_proxy_arp(peth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ p.set_ipv6_accept_ra(peth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
p.set_ipv6_autoconf(peth['ipv6_autoconf'])
# IPv6 forwarding
p.set_ipv6_forwarding(peth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
p.set_ipv6_dad_messages(peth['ipv6_dup_addr_detect'])
# assign/remove VRF (ONLY when not a member of a bridge,
# otherwise 'nomaster' removes it from it)
if not peth['is_bridge_member']:
p.set_vrf(peth['vrf'])
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in peth['ipv6_eui64_prefix_remove']:
p.del_ipv6_eui64_address(addr)
# Change interface MAC address
if peth['mac']:
p.set_mac(peth['mac'])
# Add IPv6 EUI-based addresses
for addr in peth['ipv6_eui64_prefix']:
p.add_ipv6_eui64_address(addr)
# Change interface mode
p.set_mode(peth['mode'])
# Enable/Disable interface
if peth['disable']:
p.set_admin_state('down')
else:
p.set_admin_state('up')
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in peth['address_remove']:
p.del_addr(addr)
for addr in peth['address']:
p.add_addr(addr)
# re-add ourselves to any bridge we might have fallen out of
if peth['is_bridge_member']:
p.add_to_bridge(peth['is_bridge_member'])
# apply all vlans to interface
apply_all_vlans(p, peth)
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/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index f4cd53981..3e8653d58 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -1,668 +1,674 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019 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
import netifaces
from sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
from vyos.ifconfig.afi import IP4, IP6
from vyos.configdict import list_diff
from vyos.validate import is_ipv4, is_ipv6, is_member
from vyos import ConfigError
from vyos.dicts import FixedDict
class ConfigurationState(Config):
"""
The current API require a dict to be generated by get_config()
which is then consumed by verify(), generate() and apply()
ConfiguartionState is an helper class wrapping Config and providing
an common API to this dictionary structure
Its to_dict() function return a dictionary containing three fields,
each a dict, called options, changes, actions.
options:
contains the configuration options for the dict and its value
{'options': {'commment': 'test'}} will be set if
'set interface dummy dum1 description test' was used and
the key 'commment' is used to index the description info.
changes:
per key, let us know how the data was modified using one of the action
a special key called 'section' is used to indicate what happened to the
section. for example:
'set interface dummy dum1 description test' when no interface was setup
will result in the following changes
{'changes': {'section': 'create', 'comment': 'create'}}
on an existing interface, depending if there was a description
'set interface dummy dum1 description test' will result in one of
{'changes': {'comment': 'create'}} (not present before)
{'changes': {'comment': 'static'}} (unchanged)
{'changes': {'comment': 'modify'}} (changed from half)
and 'delete interface dummy dummy1 description' will result in:
{'changes': {'comment': 'delete'}}
actions:
for each action list the configuration key which were changes
in our example if we added the 'description' and added an IP we would have
{'actions': { 'create': ['comment'], 'modify': ['addresses-add']}}
the actions are:
'create': it did not exist previously and was created
'modify': it did exist previously but its content changed
'static': it did exist and did not change
'delete': it was present but was removed from the configuration
'absent': it was not and is not present
which for each field represent how it was modified since the last commit
"""
def __init__ (self, section, default):
"""
initialise the class for a given configuration path:
>>> conf = ConfigurationState('interfaces ethernet eth1')
all further references to get_value(s) and get_effective(s)
will be for this part of the configuration (eth1)
"""
super().__init__()
self.section = section
self.default = deepcopy(default)
self.options = FixedDict(**default)
self.actions = {
'create': [], # the key did not exist and was added
'static': [], # the key exists and its value was not modfied
'modify': [], # the key exists and its value was modified
'absent': [], # the key is not present
'delete': [], # the key was present and was deleted
}
self.changes = {}
if not self.exists(section):
self.changes['section'] = 'delete'
elif self.exists_effective(section):
self.changes['section'] = 'modify'
else:
self.changes['section'] = 'create'
def _act(self, section):
"""
Returns for a given configuration field determine what happened to it
'create': it did not exist previously and was created
'modify': it did exist previously but its content changed
'static': it did exist and did not change
'delete': it was present but was removed from the configuration
'absent': it was not and is not present
"""
if self.exists(section):
if self.exists_effective(section):
if self.return_value(section) != self.return_effective_value(section):
return 'modify'
return 'static'
return 'create'
else:
if self.exists_effective(section):
return 'delete'
return 'absent'
def _action (self, name, key):
action = self._act(key)
self.changes[name] = action
self.actions[action].append(name)
return action
def _get(self, name, key, default, getter):
value = getter(key)
if not value:
if default:
self.options[name] = default
return
self.options[name] = self.default[name]
return
self.options[name] = value
def get_value(self, name, key, default=None):
"""
>>> conf.get_value('comment', 'description')
will place the string of 'interface dummy description test'
into the dictionnary entry 'comment' using Config.return_value
(the data in the configuration to apply)
"""
if self._action(name, key) in ('delete', 'absent'):
return
return self._get(name, key, default, self.return_value)
def get_values(self, name, key, default=None):
"""
>>> conf.get_values('addresses-add', 'address')
will place a list made of the IP present in 'interface dummy dum1 address'
into the dictionnary entry 'addr' using Config.return_values
(the data in the configuration to apply)
"""
if self._action(name, key) in ('delete', 'absent'):
return
return self._get(name, key, default, self.return_values)
def get_effective(self, name, key, default=None):
"""
>>> conf.get_value('comment', 'description')
will place the string of 'interface dummy description test'
into the dictionnary entry 'comment' using Config.return_effective_value
(the data in the configuration to apply)
"""
self._action(name, key)
return self._get(name, key, default, self.return_effective_value)
def get_effectives(self, name, key, default=None):
"""
>>> conf.get_effectives('addresses-add', 'address')
will place a list made of the IP present in 'interface ethernet eth1 address'
into the dictionnary entry 'addresses-add' using Config.return_effectives_value
(the data in the un-modified configuration)
"""
self._action(name, key)
return self._get(name, key, default, self.return_effectives_value)
def load(self, mapping):
"""
load will take a dictionary defining how we wish the configuration
to be parsed and apply this definition to set the data.
>>> mapping = {
'addresses-add' : ('address', True, None),
'comment' : ('description', False, 'auto'),
}
>>> conf.load(mapping)
mapping is a dictionary where each key represents the name we wish
to have (such as 'addresses-add'), with a list a content representing
how the data should be parsed:
- the configuration section name
such as 'address' under 'interface ethernet eth1'
- boolean indicating if this data can have multiple values
for 'address', True, as multiple IPs can be set
for 'description', False, as it is a single string
- default represent the default value if absent from the configuration
'None' indicate that no default should be set if the configuration
does not have the configuration section
"""
for local_name, (config_name, multiple, default) in mapping.items():
if multiple:
self.get_values(local_name, config_name, default)
else:
self.get_value(local_name, config_name, default)
def remove_default (self,*options):
"""
remove all the values which were not changed from the default
"""
for option in options:
if self.exists(option) and self_return_value(option) != self.default[option]:
continue
del self.options[option]
def to_dict (self):
"""
provide a dictionary with the generated data for the configuration
options: the configuration value for the key
changes: per key how they changed from the previous configuration
actions: per changes all the options which were changed
"""
# as we have to use a dict() for the API for verify and apply the options
return {
'options': self.options,
'changes': self.changes,
'actions': self.actions,
}
default_config_data = {
# interface definition
'vrf': '',
'addresses-add': [],
'addresses-del': [],
'state': 'up',
'dhcp-interface': '',
'link_detect': 1,
'ip': False,
'ipv6': False,
'nhrp': [],
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_forwarding': 1,
'ipv6_dad_transmits': 1,
# internal
'interfaces': [],
'tunnel': {},
'bridge': '',
# the following names are exactly matching the name
# for the ip command and must not be changed
'ifname': '',
'type': '',
'alias': '',
'mtu': '1476',
'local': '',
'remote': '',
'dev': '',
'multicast': 'disable',
'allmulticast': 'disable',
'ttl': '255',
'tos': 'inherit',
'key': '',
'encaplimit': '4',
'flowlabel': 'inherit',
'hoplimit': '64',
'tclass': 'inherit',
'6rd-prefix': '',
'6rd-relay-prefix': '',
}
# dict name -> config name, multiple values, default
mapping = {
'type': ('encapsulation', False, None),
'alias': ('description', False, None),
'mtu': ('mtu', False, None),
'local': ('local-ip', False, None),
'remote': ('remote-ip', False, None),
'multicast': ('multicast', False, None),
'dev': ('source-interface', False, None),
'ttl': ('parameters ip ttl', False, None),
'tos': ('parameters ip tos', False, None),
'key': ('parameters ip key', False, None),
'encaplimit': ('parameters ipv6 encaplimit', False, None),
'flowlabel': ('parameters ipv6 flowlabel', False, None),
'hoplimit': ('parameters ipv6 hoplimit', False, None),
'tclass': ('parameters ipv6 tclass', False, None),
'6rd-prefix': ('6rd-prefix', False, None),
'6rd-relay-prefix': ('6rd-relay-prefix', False, None),
'dhcp-interface': ('dhcp-interface', False, None),
'state': ('disable', False, 'down'),
'link_detect': ('disable-link-detect', False, 2),
'vrf': ('vrf', False, None),
'addresses-add': ('address', True, None),
'ipv6_autoconf': ('ipv6 address autoconf', False, 1),
'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0),
'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None)
}
def get_class (options):
dispatch = {
'gre': GREIf,
'gre-bridge': GRETapIf,
'ipip': IPIPIf,
'ipip6': IPIP6If,
'ip6ip6': IP6IP6If,
'ip6gre': IP6GREIf,
'sit': SitIf,
}
kls = dispatch[options['type']]
if options['type'] == 'gre' and not options['remote'] \
and not options['key'] and not options['multicast']:
# will use GreTapIf on GreIf deletion but it does not matter
return GRETapIf
elif options['type'] == 'sit' and options['6rd-prefix']:
# will use SitIf on Sit6RDIf deletion but it does not matter
return Sit6RDIf
return kls
def get_interface_ip (ifname):
if not ifname:
return ''
try:
addrs = Interface(ifname).get_addr()
if addrs:
return addrs[0].split('/')[0]
except Exception:
return ''
def get_afi (ip):
return IP6 if is_ipv6(ip) else IP4
def ip_proto (afi):
return 6 if afi == IP6 else 4
def get_config():
ifname = os.environ.get('VYOS_TAGNODE_VALUE','')
if not ifname:
raise ConfigError('Interface not specified')
conf = ConfigurationState('interfaces tunnel ' + ifname, default_config_data)
options = conf.options
changes = conf.changes
options['ifname'] = ifname
# set new configuration level
conf.set_level(conf.section)
if changes['section'] == 'delete':
conf.get_effective('type', mapping['type'][0])
conf.set_level('protocols nhrp tunnel')
options['nhrp'] = conf.list_nodes('')
return conf.to_dict()
# load all the configuration option according to the mapping
conf.load(mapping)
# remove default value if not set and not required
afi_local = get_afi(options['local'])
if afi_local == IP6:
conf.remove_default('ttl', 'tos', 'key')
if afi_local == IP4:
conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass')
# if the local-ip is not set, pick one from the interface !
# hopefully there is only one, otherwise it will not be very deterministic
# at time of writing the code currently returns ipv4 before ipv6 in the list
# XXX: There is no way to trigger an update of the interface source IP if
# XXX: the underlying interface IP address does change, I believe this
# XXX: limit/issue is present in vyatta too
if not options['local'] and options['dhcp-interface']:
# XXX: This behaviour changes from vyatta which would return 127.0.0.1 if
# XXX: the interface was not DHCP. As there is no easy way to find if an
# XXX: interface is using DHCP, and using this feature to get 127.0.0.1
# XXX: makes little sense, I feel the change in behaviour is acceptable
picked = get_interface_ip(options['dhcp-interface'])
if picked == '':
picked = '127.0.0.1'
print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead')
options['local'] = picked
options['dhcp-interface'] = ''
# get interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed
# could be done within ConfigurationState
eff_addr = conf.return_effective_values('address')
options['addresses-del'] = list_diff(eff_addr, options['addresses-add'])
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if options['ipv6_autoconf'] or 'dhcpv6' in options['address']:
+ options['ipv6_accept_ra'] = 2
+
# allmulticast fate is linked to multicast
options['allmulticast'] = options['multicast']
# check that per encapsulation all local-remote pairs are unique
conf.set_level('interfaces tunnel')
ct = conf.get_config_dict()['tunnel']
options['tunnel'] = {}
# check for bridges
options['bridge'] = is_member(conf, ifname, 'bridge')
options['interfaces'] = interfaces()
for name in ct:
tunnel = ct[name]
encap = tunnel.get('encapsulation', '')
local = tunnel.get('local-ip', '')
if not local:
local = get_interface_ip(tunnel.get('dhcp-interface', ''))
remote = tunnel.get('remote-ip', '<unset>')
pair = f'{local}-{remote}'
options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1
return conf.to_dict()
def verify(conf):
options = conf['options']
changes = conf['changes']
actions = conf['actions']
ifname = options['ifname']
iftype = options['type']
if changes['section'] == 'delete':
if ifname in options['nhrp']:
raise ConfigError((
f'Cannot delete interface tunnel {iftype} {ifname}, '
'it is used by NHRP'))
if options['bridge']:
raise ConfigError((
f'Cannot delete interface "{options["ifname"]}" as it is a '
f'member of bridge "{options["bridge"]}"!'))
# done, bail out early
return None
# tunnel encapsulation checks
if not iftype:
raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}')
if changes['type'] in ('modify', 'delete'):
# TODO: we could now deal with encapsulation modification by deleting / recreating
raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}')
if iftype != 'sit' and options['6rd-prefix']:
# XXX: should be able to remove this and let the definition catch it
print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}')
# what are the tunnel options we can set / modified / deleted
kls = get_class(options)
valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state']
if changes['section'] == 'create':
valid.extend(['type',])
valid.extend([o for o in kls.options if o not in kls.updates])
for create in actions['create']:
if create not in valid:
raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation')
for modify in actions['modify']:
if modify not in valid:
raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation')
for delete in actions['delete']:
if delete in kls.required:
raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}')
# tunnel information
tun_local = options['local']
afi_local = get_afi(tun_local)
tun_remote = options['remote'] or tun_local
afi_remote = get_afi(tun_remote)
tun_ismgre = iftype == 'gre' and not options['remote']
tun_is6rd = iftype == 'sit' and options['6rd-prefix']
tun_dev = options['dev']
# incompatible options
if not tun_local and not options['dhcp-interface'] and not tun_is6rd:
raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}')
if tun_local and options['dhcp-interface']:
raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}')
if tun_dev and iftype in ('gre-bridge', 'sit'):
raise ConfigError(f'source interface can not be used with {iftype} {ifname}')
# tunnel endpoint
if afi_local != afi_remote:
raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}')
if afi_local != kls.tunnel:
version = 4 if tun_local == IP4 else 6
raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}')
ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)])
ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)])
if tun_ismgre and afi_local == IP6:
raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}')
# check address family use
# checks are not enforced (but ip command failing) for backward compatibility
if ipv4_count and not IP4 in kls.ip:
print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}')
if ipv6_count and not IP6 in kls.ip:
print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
# vrf check
if options['vrf']:
if options['vrf'] not in options['interfaces']:
raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
if options['bridge']:
raise ConfigError((
f'Interface "{options["ifname"]}" cannot be member of VRF '
f'"{options["vrf"]}" and bridge {options["bridge"]} '
f'at the same time!'))
# bridge and address check
if ( options['bridge']
and ( options['addresses-add']
or options['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{options["name"]}" '
f'as it is a member of bridge "{options["bridge"]}"!'))
# source-interface check
if tun_dev and tun_dev not in options['interfaces']:
raise ConfigError(f'device "{dev}" does not exist')
# tunnel encapsulation check
convert = {
(6, 4, 'gre'): 'ip6gre',
(6, 6, 'gre'): 'ip6gre',
(4, 6, 'ipip'): 'ipip6',
(6, 6, 'ipip'): 'ip6ip6',
}
iprotos = []
if ipv4_count:
iprotos.append(4)
if ipv6_count:
iprotos.append(6)
for iproto in iprotos:
replace = convert.get((kls.tunnel, iproto, iftype), '')
if replace:
raise ConfigError(
f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' +
f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.'
)
# tunnel options
incompatible = []
if afi_local == IP6:
incompatible.extend(['ttl', 'tos', 'key',])
if afi_local == IP4:
incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass'])
for option in incompatible:
if option in options:
# TODO: raise converted to print as not enforced by vyatta
# raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}')
print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}')
# duplicate tunnel pairs
pair = '{}-{}'.format(options['local'], options['remote'])
if options['tunnel'].get(iftype, {}).get(pair, 0) > 1:
raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}')
return None
def generate(gre):
return None
def apply(conf):
options = conf['options']
changes = conf['changes']
actions = conf['actions']
kls = get_class(options)
# extract ifname as otherwise it is duplicated on the interface creation
ifname = options.pop('ifname')
# only the valid keys for creation of a Interface
config = dict((k, options[k]) for k in kls.options if options[k])
# setup or create the tunnel interface if it does not exist
tunnel = kls(ifname, **config)
if changes['section'] == 'delete':
tunnel.remove()
# The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup
# which identified tunnels type which were not used anymore to remove them
# (ie: gre0, gretap0, etc.) The perl code did however nothing
# This feature is also not implemented yet
return
# A GRE interface without remote will be mGRE
# if the interface does not suppor the option, it skips the change
for option in tunnel.updates:
if changes['section'] in 'create' and option in tunnel.options:
# it was setup at creation
continue
if not options[option]:
# remote can be set to '' and it would generate an invalide command
continue
tunnel.set_interface(option, options[option])
# set other interface properties
for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
- 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
if not options[option]:
# should never happen but better safe
continue
tunnel.set_interface(option, options[option])
# assign/remove VRF (ONLY when not a member of a bridge,
# otherwise 'nomaster' removes it from it)
if not options['bridge']:
tunnel.set_vrf(options['vrf'])
# Configure interface address(es)
for addr in options['addresses-del']:
tunnel.del_addr(addr)
for addr in options['addresses-add']:
tunnel.add_addr(addr)
# now bring it up (or not)
tunnel.set_admin_state(options['state'])
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/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 91682a540..84fe3dfc8 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,292 +1,300 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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 sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
'address': [],
'deleted': False,
'description': '',
'disable': False,
'group': '',
'intf': '',
'ip_arp_cache_tmo': 30,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
'source_address': '',
'source_interface': '',
'mtu': 1450,
'remote': '',
'remote_port': 8472, # The Linux implementation of VXLAN pre-dates
# the IANA's selection of a standard destination port
'vni': ''
}
def get_config():
vxlan = 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')
vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if interface is member if a bridge
vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge')
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
vxlan['deleted'] = True
return vxlan
# set new configuration level
conf.set_level('interfaces vxlan ' + vxlan['intf'])
# retrieve configured interface addresses
if conf.exists('address'):
vxlan['address'] = conf.return_values('address')
# retrieve interface description
if conf.exists('description'):
vxlan['description'] = conf.return_value('description')
# Disable this interface
if conf.exists('disable'):
vxlan['disable'] = True
# VXLAN multicast grou
if conf.exists('group'):
vxlan['group'] = conf.return_value('group')
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
vxlan['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# ARP filter configuration
if conf.exists('ip disable-arp-filter'):
vxlan['ip_disable_arp_filter'] = 0
# ARP enable accept
if conf.exists('ip enable-arp-accept'):
vxlan['ip_enable_arp_accept'] = 1
# ARP enable announce
if conf.exists('ip enable-arp-announce'):
vxlan['ip_enable_arp_announce'] = 1
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
vxlan['ip_enable_arp_ignore'] = 1
# Enable proxy-arp on this interface
if conf.exists('ip enable-proxy-arp'):
vxlan['ip_proxy_arp'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
vxlan['ipv6_autoconf'] = 1
# Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
# Remove the default link-local address if set.
if not ( conf.exists('ipv6 address no-default-link-local')
or vxlan['is_bridge_member'] ):
# add the link-local by default to make IPv6 work
vxlan['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
vxlan['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']:
+ vxlan['ipv6_accept_ra'] = 2
+
# VXLAN source address
if conf.exists('source-address'):
vxlan['source_address'] = conf.return_value('source-address')
# VXLAN underlay interface
if conf.exists('source-interface'):
vxlan['source_interface'] = conf.return_value('source-interface')
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
vxlan['mtu'] = int(conf.return_value('mtu'))
# Remote address of VXLAN tunnel
if conf.exists('remote'):
vxlan['remote'] = conf.return_value('remote')
# Remote port of VXLAN tunnel
if conf.exists('port'):
vxlan['remote_port'] = int(conf.return_value('port'))
# Virtual Network Identifier
if conf.exists('vni'):
vxlan['vni'] = conf.return_value('vni')
return vxlan
def verify(vxlan):
if vxlan['deleted']:
if vxlan['is_bridge_member']:
raise ConfigError((
f'Cannot delete interface "{vxlan["intf"]}" as it is a '
f'member of bridge "{vxlan["is_bridge_member"]}"!'))
return None
if vxlan['mtu'] < 1500:
print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
if vxlan['group']:
if not vxlan['source_interface']:
raise ConfigError('Multicast VXLAN requires an underlaying interface ')
if not vxlan['source_interface'] in interfaces():
raise ConfigError('VXLAN source interface does not exist')
if not (vxlan['group'] or vxlan['remote'] or vxlan['source_address']):
raise ConfigError('Group, remote or source-address must be configured')
if not vxlan['vni']:
raise ConfigError('Must configure VNI for VXLAN')
if vxlan['source_interface']:
# VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
# if our configured MTU is at least 50 bytes less
underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu())
if underlay_mtu < (vxlan['mtu'] + 50):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
if ( vxlan['is_bridge_member']
and ( vxlan['address']
or vxlan['ipv6_eui64_prefix']
or vxlan['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{vxlan["intf"]}" '
f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!'))
return None
def generate(vxlan):
return None
def apply(vxlan):
# Check if the VXLAN interface already exists
if vxlan['intf'] in interfaces():
v = VXLANIf(vxlan['intf'])
# VXLAN is super picky and the tunnel always needs to be recreated,
# thus we can simply always delete it first.
v.remove()
if not vxlan['deleted']:
# VXLAN interface needs to be created on-block
# instead of passing a ton of arguments, I just use a dict
# that is managed by vyos.ifconfig
conf = deepcopy(VXLANIf.get_config())
# Assign VXLAN instance configuration parameters to config dict
conf['vni'] = vxlan['vni']
conf['group'] = vxlan['group']
conf['src_address'] = vxlan['source_address']
conf['src_interface'] = vxlan['source_interface']
conf['remote'] = vxlan['remote']
conf['port'] = vxlan['remote_port']
# Finally create the new interface
v = VXLANIf(vxlan['intf'], **conf)
# update interface description used e.g. by SNMP
v.set_alias(vxlan['description'])
# Maximum Transfer Unit (MTU)
v.set_mtu(vxlan['mtu'])
# configure ARP cache timeout in milliseconds
v.set_arp_cache_tmo(vxlan['ip_arp_cache_tmo'])
# configure ARP filter configuration
v.set_arp_filter(vxlan['ip_disable_arp_filter'])
# configure ARP accept
v.set_arp_accept(vxlan['ip_enable_arp_accept'])
# configure ARP announce
v.set_arp_announce(vxlan['ip_enable_arp_announce'])
# configure ARP ignore
v.set_arp_ignore(vxlan['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
v.set_proxy_arp(vxlan['ip_proxy_arp'])
+ # IPv6 accept RA
+ v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra'])
# IPv6 address autoconfiguration
v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
# IPv6 forwarding
v.set_ipv6_forwarding(vxlan['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
v.set_ipv6_dad_messages(vxlan['ipv6_dup_addr_detect'])
# Configure interface address(es) - no need to implicitly delete the
# old addresses as they have already been removed by deleting the
# interface above
for addr in vxlan['address']:
v.add_addr(addr)
# IPv6 EUI-based addresses
for addr in vxlan['ipv6_eui64_prefix']:
v.add_ipv6_eui64_address(addr)
# As the VXLAN interface is always disabled first when changing
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not vxlan['disable']:
v.set_admin_state('up')
# re-add ourselves to any bridge we might have fallen out of
if vxlan['is_bridge_member']:
v.add_to_bridge(vxlan['is_bridge_member'])
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/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 03f66dd81..70d46d061 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -1,691 +1,694 @@
#!/usr/bin/env python3
#
# Copyright (C) 2019-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 sys import exit
from re import findall
from copy import deepcopy
from netifaces import interfaces
from netaddr import EUI, mac_unix_expanded
from vyos.config import Config
from vyos.configdict import list_diff, intf_to_dict, add_to_dict
from vyos.ifconfig import WiFiIf, Section
from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.template import render
from vyos.util import chown, call
from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
'cap_ht' : False,
'cap_ht_40mhz_incapable' : False,
'cap_ht_powersave' : False,
'cap_ht_chan_set_width' : '',
'cap_ht_delayed_block_ack' : False,
'cap_ht_dsss_cck_40' : False,
'cap_ht_greenfield' : False,
'cap_ht_ldpc' : False,
'cap_ht_lsig_protection' : False,
'cap_ht_max_amsdu' : '',
'cap_ht_short_gi' : [],
'cap_ht_smps' : '',
'cap_ht_stbc_rx' : '',
'cap_ht_stbc_tx' : False,
'cap_req_ht' : False,
'cap_req_vht' : False,
'cap_vht' : False,
'cap_vht_antenna_cnt' : '',
'cap_vht_antenna_fixed' : False,
'cap_vht_beamform' : '',
'cap_vht_center_freq_1' : '',
'cap_vht_center_freq_2' : '',
'cap_vht_chan_set_width' : '',
'cap_vht_ldpc' : False,
'cap_vht_link_adaptation' : '',
'cap_vht_max_mpdu_exp' : '',
'cap_vht_max_mpdu' : '',
'cap_vht_short_gi' : [],
'cap_vht_stbc_rx' : '',
'cap_vht_stbc_tx' : False,
'cap_vht_tx_powersave' : False,
'cap_vht_vht_cf' : False,
'channel': '',
'country_code': '',
'description': '',
'deleted': False,
'dhcp_client_id': '',
'dhcp_hostname': '',
'dhcp_vendor_class_id': '',
'dhcpv6_prm_only': False,
'dhcpv6_temporary': False,
'disable': False,
'disable_broadcast_ssid' : False,
'disable_link_detect' : 1,
'expunge_failing_stations' : False,
'hw_id' : '',
'intf': '',
'isolate_stations' : False,
'ip_disable_arp_filter': 1,
'ip_enable_arp_accept': 0,
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': [],
'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
'mac' : '',
'max_stations' : '',
'mgmt_frame_protection' : 'disabled',
'mode' : 'g',
'phy' : '',
'reduce_transmit_power' : '',
'sec_wep' : False,
'sec_wep_key' : [],
'sec_wpa' : False,
'sec_wpa_cipher' : [],
'sec_wpa_mode' : 'both',
'sec_wpa_passphrase' : '',
'sec_wpa_radius' : [],
'ssid' : '',
'op_mode' : 'monitor',
'vif': {},
'vif_remove': [],
'vif_s': {},
'vif_s_remove': [],
'vrf': ''
}
def get_conf_file(conf_type, intf):
cfg_dir = '/run/' + conf_type
# create directory on demand
if not os.path.exists(cfg_dir):
os.makedirs(cfg_dir, 0o755)
chown(cfg_dir, 'root', 'vyattacfg')
cfg_file = cfg_dir + r'/{}.conf'.format(intf)
return cfg_file
def get_config():
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
ifname = os.environ['VYOS_TAGNODE_VALUE']
conf = Config()
# check if wireless interface has been removed
cfg_base = ['interfaces', 'wireless ', ifname]
if not conf.exists(cfg_base):
wifi = deepcopy(default_config_data)
wifi['intf'] = ifname
wifi['deleted'] = True
# we need to know if we're a bridge member so we can refuse deletion
wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge')
# we can not bail out early as wireless interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
# Thus we need to remove individual settings
return wifi
# set new configuration level
conf.set_level(cfg_base)
# get common interface settings
wifi, disabled = intf_to_dict(conf, default_config_data)
# 40MHz intolerance, use 20MHz only
if conf.exists('capabilities ht 40mhz-incapable'):
wifi['cap_ht'] = True
wifi['cap_ht_40mhz_incapable'] = True
# WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD]
if conf.exists('capabilities ht auto-powersave'):
wifi['cap_ht'] = True
wifi['cap_ht_powersave'] = True
# Supported channel set width
if conf.exists('capabilities ht channel-set-width'):
wifi['cap_ht'] = True
wifi['cap_ht_chan_set_width'] = conf.return_values('capabilities ht channel-set-width')
# HT-delayed Block Ack
if conf.exists('capabilities ht delayed-block-ack'):
wifi['cap_ht'] = True
wifi['cap_ht_delayed_block_ack'] = True
# DSSS/CCK Mode in 40 MHz
if conf.exists('capabilities ht dsss-cck-40'):
wifi['cap_ht'] = True
wifi['cap_ht_dsss_cck_40'] = True
# HT-greenfield capability
if conf.exists('capabilities ht greenfield'):
wifi['cap_ht'] = True
wifi['cap_ht_greenfield'] = True
# LDPC coding capability
if conf.exists('capabilities ht ldpc'):
wifi['cap_ht'] = True
wifi['cap_ht_ldpc'] = True
# L-SIG TXOP protection capability
if conf.exists('capabilities ht lsig-protection'):
wifi['cap_ht'] = True
wifi['cap_ht_lsig_protection'] = True
# Set Maximum A-MSDU length
if conf.exists('capabilities ht max-amsdu'):
wifi['cap_ht'] = True
wifi['cap_ht_max_amsdu'] = conf.return_value('capabilities ht max-amsdu')
# Short GI capabilities
if conf.exists('capabilities ht short-gi'):
wifi['cap_ht'] = True
wifi['cap_ht_short_gi'] = conf.return_values('capabilities ht short-gi')
# Spatial Multiplexing Power Save (SMPS) settings
if conf.exists('capabilities ht smps'):
wifi['cap_ht'] = True
wifi['cap_ht_smps'] = conf.return_value('capabilities ht smps')
# Support for receiving PPDU using STBC (Space Time Block Coding)
if conf.exists('capabilities ht stbc rx'):
wifi['cap_ht'] = True
wifi['cap_ht_stbc_rx'] = conf.return_value('capabilities ht stbc rx')
# Support for sending PPDU using STBC (Space Time Block Coding)
if conf.exists('capabilities ht stbc tx'):
wifi['cap_ht'] = True
wifi['cap_ht_stbc_tx'] = True
# Require stations to support HT PHY (reject association if they do not)
if conf.exists('capabilities require-ht'):
wifi['cap_req_ht'] = True
# Require stations to support VHT PHY (reject association if they do not)
if conf.exists('capabilities require-vht'):
wifi['cap_req_vht'] = True
# Number of antennas on this card
if conf.exists('capabilities vht antenna-count'):
wifi['cap_vht'] = True
wifi['cap_vht_antenna_cnt'] = conf.return_value('capabilities vht antenna-count')
# set if antenna pattern does not change during the lifetime of an association
if conf.exists('capabilities vht antenna-pattern-fixed'):
wifi['cap_vht'] = True
wifi['cap_vht_antenna_fixed'] = True
# Beamforming capabilities
if conf.exists('capabilities vht beamform'):
wifi['cap_vht'] = True
wifi['cap_vht_beamform'] = conf.return_values('capabilities vht beamform')
# VHT operating channel center frequency - center freq 1 (for use with 80, 80+80 and 160 modes)
if conf.exists('capabilities vht center-channel-freq freq-1'):
wifi['cap_vht'] = True
wifi['cap_vht_center_freq_1'] = conf.return_value('capabilities vht center-channel-freq freq-1')
# VHT operating channel center frequency - center freq 2 (for use with the 80+80 mode)
if conf.exists('capabilities vht center-channel-freq freq-2'):
wifi['cap_vht'] = True
wifi['cap_vht_center_freq_2'] = conf.return_value('capabilities vht center-channel-freq freq-2')
# VHT operating Channel width
if conf.exists('capabilities vht channel-set-width'):
wifi['cap_vht'] = True
wifi['cap_vht_chan_set_width'] = conf.return_value('capabilities vht channel-set-width')
# LDPC coding capability
if conf.exists('capabilities vht ldpc'):
wifi['cap_vht'] = True
wifi['cap_vht_ldpc'] = True
# VHT link adaptation capabilities
if conf.exists('capabilities vht link-adaptation'):
wifi['cap_vht'] = True
wifi['cap_vht_link_adaptation'] = conf.return_value('capabilities vht link-adaptation')
# Set the maximum length of A-MPDU pre-EOF padding that the station can receive
if conf.exists('capabilities vht max-mpdu-exp'):
wifi['cap_vht'] = True
wifi['cap_vht_max_mpdu_exp'] = conf.return_value('capabilities vht max-mpdu-exp')
# Increase Maximum MPDU length
if conf.exists('capabilities vht max-mpdu'):
wifi['cap_vht'] = True
wifi['cap_vht_max_mpdu'] = conf.return_value('capabilities vht max-mpdu')
# Increase Maximum MPDU length
if conf.exists('capabilities vht short-gi'):
wifi['cap_vht'] = True
wifi['cap_vht_short_gi'] = conf.return_values('capabilities vht short-gi')
# Support for receiving PPDU using STBC (Space Time Block Coding)
if conf.exists('capabilities vht stbc rx'):
wifi['cap_vht'] = True
wifi['cap_vht_stbc_rx'] = conf.return_value('capabilities vht stbc rx')
# Support for the transmission of at least 2x1 STBC (Space Time Block Coding)
if conf.exists('capabilities vht stbc tx'):
wifi['cap_vht'] = True
wifi['cap_vht_stbc_tx'] = True
# Support for VHT TXOP Power Save Mode
if conf.exists('capabilities vht tx-powersave'):
wifi['cap_vht'] = True
wifi['cap_vht_tx_powersave'] = True
# STA supports receiving a VHT variant HT Control field
if conf.exists('capabilities vht vht-cf'):
wifi['cap_vht'] = True
wifi['cap_vht_vht_cf'] = True
# Wireless radio channel
if conf.exists('channel'):
wifi['channel'] = conf.return_value('channel')
# Disable broadcast of SSID from access-point
if conf.exists('disable-broadcast-ssid'):
wifi['disable_broadcast_ssid'] = True
# Disassociate stations based on excessive transmission failures
if conf.exists('expunge-failing-stations'):
wifi['expunge_failing_stations'] = True
# retrieve real hardware address
if conf.exists('hw-id'):
wifi['hw_id'] = conf.return_value('hw-id')
# Isolate stations on the AP so they cannot see each other
if conf.exists('isolate-stations'):
wifi['isolate_stations'] = True
# Wireless physical device
if conf.exists('physical-device'):
wifi['phy'] = conf.return_value('physical-device')
# Maximum number of wireless radio stations
if conf.exists('max-stations'):
wifi['max_stations'] = conf.return_value('max-stations')
# Management Frame Protection (MFP) according to IEEE 802.11w
if conf.exists('mgmt-frame-protection'):
wifi['mgmt_frame_protection'] = conf.return_value('mgmt-frame-protection')
# Wireless radio mode
if conf.exists('mode'):
wifi['mode'] = conf.return_value('mode')
# Transmission power reduction in dBm
if conf.exists('reduce-transmit-power'):
wifi['reduce_transmit_power'] = conf.return_value('reduce-transmit-power')
# WEP enabled?
if conf.exists('security wep'):
wifi['sec_wep'] = True
# WEP encryption key(s)
if conf.exists('security wep key'):
wifi['sec_wep_key'] = conf.return_values('security wep key')
# WPA enabled?
if conf.exists('security wpa'):
wifi['sec_wpa'] = True
# WPA Cipher suite
if conf.exists('security wpa cipher'):
wifi['sec_wpa_cipher'] = conf.return_values('security wpa cipher')
# WPA mode
if conf.exists('security wpa mode'):
wifi['sec_wpa_mode'] = conf.return_value('security wpa mode')
# WPA default ciphers depend on WPA mode
if not wifi['sec_wpa_cipher']:
if wifi['sec_wpa_mode'] == 'wpa':
wifi['sec_wpa_cipher'].append('TKIP')
wifi['sec_wpa_cipher'].append('CCMP')
elif wifi['sec_wpa_mode'] == 'wpa2':
wifi['sec_wpa_cipher'].append('CCMP')
elif wifi['sec_wpa_mode'] == 'both':
wifi['sec_wpa_cipher'].append('CCMP')
wifi['sec_wpa_cipher'].append('TKIP')
# WPA Group Cipher suite
if conf.exists('security wpa group-cipher'):
wifi['sec_wpa_group_cipher'] = conf.return_values('security wpa group-cipher')
# WPA personal shared pass phrase
if conf.exists('security wpa passphrase'):
wifi['sec_wpa_passphrase'] = conf.return_value('security wpa passphrase')
# WPA RADIUS source address
if conf.exists('security wpa radius source-address'):
wifi['sec_wpa_radius_source'] = conf.return_value('security wpa radius source-address')
# WPA RADIUS server
for server in conf.list_nodes('security wpa radius server'):
# set new configuration level
conf.set_level(cfg_base + ' security wpa radius server ' + server)
radius = {
'server' : server,
'acc_port' : '',
'disabled': False,
'port' : 1812,
'key' : ''
}
# RADIUS server port
if conf.exists('port'):
radius['port'] = int(conf.return_value('port'))
# receive RADIUS accounting info
if conf.exists('accounting'):
radius['acc_port'] = radius['port'] + 1
# Check if RADIUS server was temporary disabled
if conf.exists(['disable']):
radius['disabled'] = True
# RADIUS server shared-secret
if conf.exists('key'):
radius['key'] = conf.return_value('key')
# append RADIUS server to list of servers
wifi['sec_wpa_radius'].append(radius)
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# Wireless access-point service set identifier (SSID)
if conf.exists('ssid'):
wifi['ssid'] = conf.return_value('ssid')
# Wireless device type for this interface
if conf.exists('type'):
tmp = conf.return_value('type')
if tmp == 'access-point':
tmp = 'ap'
wifi['op_mode'] = tmp
# retrieve configured regulatory domain
conf.set_level('system')
if conf.exists('wifi-regulatory-domain'):
wifi['country_code'] = conf.return_value('wifi-regulatory-domain')
return wifi
def verify(wifi):
if wifi['deleted']:
if wifi['is_bridge_member']:
raise ConfigError((
f'Cannot delete interface "{wifi["intf"]}" as it is a '
f'member of bridge "{wifi["is_bridge_member"]}"!'))
return None
if wifi['op_mode'] != 'monitor' and not wifi['ssid']:
raise ConfigError('SSID must be set for {}'.format(wifi['intf']))
if not wifi['phy']:
raise ConfigError('You must specify physical-device')
if not wifi['mode']:
raise ConfigError('You must specify a WiFi mode')
if wifi['op_mode'] == 'ap':
c = Config()
if not c.exists('system wifi-regulatory-domain'):
raise ConfigError('Wireless regulatory domain is mandatory,\n' \
'use "set system wifi-regulatory-domain".')
if not wifi['channel']:
raise ConfigError('Channel must be set for {}'.format(wifi['intf']))
if len(wifi['sec_wep_key']) > 4:
raise ConfigError('No more then 4 WEP keys configurable')
if wifi['cap_vht'] and not wifi['cap_ht']:
raise ConfigError('Specify HT flags if you want to use VHT!')
if wifi['cap_vht_beamform'] and wifi['cap_vht_antenna_cnt'] == 1:
raise ConfigError('Cannot use beam forming with just one antenna!')
if wifi['cap_vht_beamform'] == 'single-user-beamformer' and wifi['cap_vht_antenna_cnt'] < 3:
# Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705
raise ConfigError('Single-user beam former requires at least 3 antennas!')
if wifi['sec_wep'] and (len(wifi['sec_wep_key']) == 0):
raise ConfigError('Missing WEP keys')
if wifi['sec_wpa'] and not (wifi['sec_wpa_passphrase'] or wifi['sec_wpa_radius']):
raise ConfigError('Misssing WPA key or RADIUS server')
for radius in wifi['sec_wpa_radius']:
if not radius['key']:
raise ConfigError('Misssing RADIUS shared secret key for server: {}'.format(radius['server']))
if ( wifi['is_bridge_member']
and ( wifi['address']
or wifi['ipv6_eui64_prefix']
or wifi['ipv6_autoconf'] ) ):
raise ConfigError((
f'Cannot assign address to interface "{wifi["intf"]}" '
f'as it is a member of bridge "{wifi["is_bridge_member"]}"!'))
if wifi['vrf']:
if wifi['vrf'] not in interfaces():
raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist')
if wifi['is_bridge_member']:
raise ConfigError((
f'Interface "{wifi["intf"]}" cannot be member of VRF '
f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} '
f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
conf = Config()
# Only one wireless interface per phy can be in station mode
base = ['interfaces', 'wireless']
for phy in os.listdir('/sys/class/ieee80211'):
stations = []
for wlan in conf.list_nodes(base):
# the following node is mandatory
if conf.exists(base + [wlan, 'physical-device', phy]):
tmp = conf.return_value(base + [wlan, 'type'])
if tmp == 'station':
stations.append(wlan)
if len(stations) > 1:
raise ConfigError('Only one station per wireless physical interface possible!')
return None
def generate(wifi):
interface = wifi['intf']
# always stop hostapd service first before reconfiguring it
call(f'systemctl stop hostapd@{interface}.service')
# always stop wpa_supplicant service first before reconfiguring it
call(f'systemctl stop wpa_supplicant@{interface}.service')
# Delete config files if interface is removed
if wifi['deleted']:
if os.path.isfile(get_conf_file('hostapd', interface)):
os.unlink(get_conf_file('hostapd', interface))
if os.path.isfile(get_conf_file('wpa_supplicant', interface)):
os.unlink(get_conf_file('wpa_supplicant', interface))
return None
if not wifi['mac']:
# http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd
# generate locally administered MAC address from used phy interface
with open('/sys/class/ieee80211/{}/addresses'.format(wifi['phy']), 'r') as f:
# some PHYs tend to have multiple interfaces and thus supply multiple MAC
# addresses - we only need the first one for our calculation
tmp = f.readline().rstrip()
tmp = EUI(tmp).value
# mask last nibble from the MAC address
tmp &= 0xfffffffffff0
# set locally administered bit in MAC address
tmp |= 0x020000000000
# we now need to add an offset to our MAC address indicating this
# subinterfaces index
tmp += int(findall(r'\d+', interface)[0])
# convert integer to "real" MAC address representation
mac = EUI(hex(tmp).split('x')[-1])
# change dialect to use : as delimiter instead of -
mac.dialect = mac_unix_expanded
wifi['mac'] = str(mac)
# render appropriate new config files depending on access-point or station mode
if wifi['op_mode'] == 'ap':
conf = get_conf_file('hostapd', interface)
render(conf, 'wifi/hostapd.conf.tmpl', wifi)
elif wifi['op_mode'] == 'station':
conf = get_conf_file('wpa_supplicant', interface)
render(conf, 'wifi/wpa_supplicant.conf.tmpl', wifi)
return None
def apply(wifi):
interface = wifi['intf']
if wifi['deleted']:
w = WiFiIf(interface)
# delete interface
w.remove()
else:
# WiFi interface needs to be created on-block (e.g. mode or physical
# interface) instead of passing a ton of arguments, I just use a dict
# that is managed by vyos.ifconfig
conf = deepcopy(WiFiIf.get_config())
# Assign WiFi instance configuration parameters to config dict
conf['phy'] = wifi['phy']
# Finally create the new interface
w = WiFiIf(interface, **conf)
# assign/remove VRF (ONLY when not a member of a bridge,
# otherwise 'nomaster' removes it from it)
if not wifi['is_bridge_member']:
w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
if wifi['dhcp_client_id']:
w.dhcp.v4.options['client_id'] = wifi['dhcp_client_id']
if wifi['dhcp_hostname']:
w.dhcp.v4.options['hostname'] = wifi['dhcp_hostname']
if wifi['dhcp_vendor_class_id']:
w.dhcp.v4.options['vendor_class_id'] = wifi['dhcp_vendor_class_id']
if wifi['dhcpv6_prm_only']:
w.dhcp.v6.options['dhcpv6_prm_only'] = True
if wifi['dhcpv6_temporary']:
w.dhcp.v6.options['dhcpv6_temporary'] = True
# ignore link state changes
w.set_link_detect(wifi['disable_link_detect'])
# Delete old IPv6 EUI64 addresses before changing MAC
for addr in wifi['ipv6_eui64_prefix_remove']:
w.del_ipv6_eui64_address(addr)
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if wifi['mac']:
w.set_mac(wifi['mac'])
elif wifi['hw_id']:
w.set_mac(wifi['hw_id'])
# Add IPv6 EUI-based addresses
for addr in wifi['ipv6_eui64_prefix']:
w.add_ipv6_eui64_address(addr)
# configure ARP filter configuration
w.set_arp_filter(wifi['ip_disable_arp_filter'])
# configure ARP accept
w.set_arp_accept(wifi['ip_enable_arp_accept'])
# configure ARP announce
w.set_arp_announce(wifi['ip_enable_arp_announce'])
# configure ARP ignore
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ w.set_ipv6_accept_ra(wifi['ipv6_accept_ra'])
# IPv6 address autoconfiguration
w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
# IPv6 forwarding
w.set_ipv6_forwarding(wifi['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
w.set_ipv6_dad_messages(wifi['ipv6_dup_addr_detect'])
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in wifi['address_remove']:
w.del_addr(addr)
for addr in wifi['address']:
w.add_addr(addr)
# apply all vlans to interface
apply_all_vlans(w, wifi)
# Enable/Disable interface - interface is always placed in
# administrative down state in WiFiIf class
if not wifi['disable']:
w.set_admin_state('up')
# Physical interface is now configured. Proceed by starting hostapd or
# wpa_supplicant daemon. When type is monitor we can just skip this.
if wifi['op_mode'] == 'ap':
call(f'systemctl start hostapd@{interface}.service')
elif wifi['op_mode'] == 'station':
call(f'systemctl start wpa_supplicant@{interface}.service')
return None
if __name__ == '__main__':
try:
c = get_config()
verify(c)
generate(c)
apply(c)
except ConfigError as e:
print(e)
exit(1)
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 26, 12:33 PM (1 d, 17 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4285156
Default Alt Text
(225 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment