Page Menu
Home
VyOS Platform
Search
Configure Global Search
Log In
Files
F119064099
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
178 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/python/vyos/util.py b/python/vyos/util.py
index c70bff091..b1d95fbbf 100644
--- a/python/vyos/util.py
+++ b/python/vyos/util.py
@@ -1,455 +1,437 @@
# 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/>.
import os
#
# NOTE: Do not import full classes here, move your import to the function
# where it is used so it is as local as possible to the execution
#
# There is many (too many) ways to run command with python
# os.system, subprocess.Popen, subproces.{run,call,check_output}
# which all have slighty different behaviour
from subprocess import Popen, PIPE, STDOUT, DEVNULL
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=None, decode=None):
"""
popen is a wrapper helper aound subprocess.Popen
with it default setting it will return a tuple (out, err)
out: the output of the program run
err: the error code returned by the program
it can be affected by the following flags:
shell: do not try to auto-detect if a shell is required
for example if a pipe (|) or redirection (>, >>) is used
input: data to sent to the child process via STDIN
the data should be bytes but string will be converted
timeout: time after which the command will be considered to have failed
env: mapping that defines the environment variables for the new process
stdout: define how the output of the program should be handled
- PIPE (default), sends stdout to the output
- DEVNULL, discard the output
stderr: define how the output of the program should be handled
- None (default), send/merge the data to/with stderr
- PIPE, popen will append it to output
- STDOUT, send the data to be merged with stdout
- DEVNULL, discard the output
decode: specify the expected text encoding (utf-8, ascii, ...)
usage:
to get both stdout, and stderr: popen('command', stdout=PIPE, stderr=STDOUT)
to discard stdout and get stderr: popen('command', stdout=DEVNUL, stderr=PIPE)
"""
from vyos import debug
# log if the flag is set, otherwise log if command is set
if not debug.enabled(flag):
flag = 'command'
cmd_msg = f"cmd '{command}'"
debug.message(cmd_msg, flag)
use_shell = shell
stdin = None
if shell is None:
use_shell = False
if ' ' in command:
use_shell = True
if env:
use_shell = True
if input:
stdin = PIPE
input = input.encode() if type(input) is str else input
p = Popen(
command,
stdin=stdin, stdout=stdout, stderr=stderr,
env=env, shell=use_shell,
)
tmp = p.communicate(input, timeout)
out1 = b''
out2 = b''
if stdout == PIPE:
out1 = tmp[0]
if stderr == PIPE:
out2 += tmp[1]
decoded1 = out1.decode(decode) if decode else out1.decode()
decoded2 = out2.decode(decode) if decode else out2.decode()
decoded1 = decoded1.replace('\r\n', '\n').strip()
decoded2 = decoded2.replace('\r\n', '\n').strip()
nl = '\n' if decoded1 and decoded2 else ''
decoded = decoded1 + nl + decoded2
if decoded:
ret_msg = f"returned:\n{decoded}"
debug.message(ret_msg, flag)
return decoded, p.returncode
def run(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=DEVNULL, stderr=None, decode=None):
"""
A wrapper around vyos.util.popen, which discard the stdout and
will return the error code of a command
"""
_, code = popen(
command, flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,
decode=decode,
)
return code
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=None, decode=None,
raising=None, message=''):
"""
A wrapper around vyos.util.popen, which returns the stdout and
will raise the error code of a command
raising: specify which call should be used when raising (default is OSError)
the class should only require a string as parameter
"""
decoded, code = popen(
command, flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,
decode=decode,
)
if code != 0:
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'
feedback += f'returned: {decoded}\n'
feedback += f'exit code: {code}'
if raising is None:
# error code can be recovered with .errno
raise OSError(code, feedback)
else:
raise raising(feedback)
return decoded
def call(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=None, decode=None):
"""
A wrapper around vyos.util.popen, which print the stdout and
will return the error code of a command
"""
out, code = popen(
command, flag,
stdout=stdout, stderr=stderr,
input=input, timeout=timeout,
env=env, shell=shell,
decode=decode,
)
print(out)
return code
def read_file(path):
""" Read a file to string """
with open(path, 'r') as f:
data = f.read().strip()
return data
def chown(path, user, group):
""" change file/directory owner """
from pwd import getpwnam
from grp import getgrnam
if os.path.exists(path):
uid = getpwnam(user).pw_uid
gid = getgrnam(group).gr_gid
os.chown(path, uid, gid)
def chmod_600(path):
""" make file only read/writable by owner """
from stat import S_IRUSR, S_IWUSR
if os.path.exists(path):
bitmask = S_IRUSR | S_IWUSR
os.chmod(path, bitmask)
def chmod_750(path):
""" make file/directory only executable to user and group """
from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP
if os.path.exists(path):
bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP
os.chmod(path, bitmask)
def chmod_755(path):
""" make file executable by all """
from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH
if os.path.exists(path):
bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP | \
S_IROTH | S_IXOTH
os.chmod(path, bitmask)
def colon_separated_to_dict(data_string, uniquekeys=False):
""" Converts a string containing newline-separated entries
of colon-separated key-value pairs into a dict.
Such files are common in Linux /proc filesystem
Args:
data_string (str): data string
uniquekeys (bool): whether to insist that keys are unique or not
Returns: dict
Raises:
ValueError: if uniquekeys=True and the data string has
duplicate keys.
Note:
If uniquekeys=True, then dict entries are always strings,
otherwise they are always lists of strings.
"""
import re
key_value_re = re.compile('([^:]+)\s*\:\s*(.*)')
data_raw = re.split('\n', data_string)
data = {}
for l in data_raw:
l = l.strip()
if l:
match = re.match(key_value_re, l)
if match:
key = match.groups()[0].strip()
value = match.groups()[1].strip()
if key in data.keys():
if uniquekeys:
raise ValueError("Data string has duplicate keys: {0}".format(key))
else:
data[key].append(value)
else:
if uniquekeys:
data[key] = value
else:
data[key] = [value]
else:
pass
return data
def process_running(pid_file):
""" Checks if a process with PID in pid_file is running """
from psutil import pid_exists
if not os.path.isfile(pid_file):
return False
with open(pid_file, 'r') as f:
pid = f.read().strip()
return pid_exists(int(pid))
def process_named_running(name):
""" Checks if process with given name is running and returns its PID.
If Process is not running, return None
"""
from psutil import process_iter
for p in process_iter():
if name in p.name():
return p.pid
return None
def seconds_to_human(s, separator=""):
""" Converts number of seconds passed to a human-readable
interval such as 1w4d18h35m59s
"""
s = int(s)
week = 60 * 60 * 24 * 7
day = 60 * 60 * 24
hour = 60 * 60
remainder = 0
result = ""
weeks = s // week
if weeks > 0:
result = "{0}w".format(weeks)
s = s % week
days = s // day
if days > 0:
result = "{0}{1}{2}d".format(result, separator, days)
s = s % day
hours = s // hour
if hours > 0:
result = "{0}{1}{2}h".format(result, separator, hours)
s = s % hour
minutes = s // 60
if minutes > 0:
result = "{0}{1}{2}m".format(result, separator, minutes)
s = s % 60
seconds = s
if seconds > 0:
result = "{0}{1}{2}s".format(result, separator, seconds)
return result
def get_cfg_group_id():
from grp import getgrnam
from vyos.defaults import cfg_group
group_data = getgrnam(cfg_group)
return group_data.gr_gid
def file_is_persistent(path):
import re
if not re.match(r'^(/config|/opt/vyatta/etc/config)', os.path.dirname(path)):
warning = "Warning: file {0} is outside the /config directory\n".format(path)
warning += "It will not be automatically migrated to a new image on system update"
return (False, warning)
else:
return (True, None)
def commit_in_progress():
""" Not to be used in normal op mode scripts! """
# The CStore backend locks the config by opening a file
# The file is not removed after commit, so just checking
# if it exists is insufficient, we need to know if it's open by anyone
# There are two ways to check if any other process keeps a file open.
# The first one is to try opening it and see if the OS objects.
# That's faster but prone to race conditions and can be intrusive.
# The other one is to actually check if any process keeps it open.
# It's non-intrusive but needs root permissions, else you can't check
# processes of other users.
#
# Since this will be used in scripts that modify the config outside of the CLI
# framework, those knowingly have root permissions.
# For everything else, we add a safeguard.
from psutil import process_iter, NoSuchProcess
from vyos.defaults import commit_lock
idu = cmd('/usr/bin/id -u')
if idu != '0':
raise OSError("This functions needs root permissions to return correct results")
for proc in process_iter():
try:
files = proc.open_files()
if files:
for f in files:
if f.path == commit_lock:
return True
except NoSuchProcess as err:
# Process died before we could examine it
pass
# Default case
return False
def wait_for_commit_lock():
""" Not to be used in normal op mode scripts! """
from time import sleep
# Very synchronous approach to multiprocessing
while commit_in_progress():
sleep(1)
def ask_yes_no(question, default=False) -> bool:
"""Ask a yes/no question via input() and return their answer."""
from sys import stdout
default_msg = "[Y/n]" if default else "[y/N]"
while True:
stdout.write("%s %s " % (question, default_msg))
c = input().lower()
if c == '':
return default
elif c in ("y", "ye", "yes"):
return True
elif c in ("n", "no"):
return False
else:
stdout.write("Please respond with yes/y or no/n\n")
def is_admin() -> bool:
"""Look if current user is in sudo group"""
from getpass import getuser
from grp import getgrnam
current_user = getuser()
(_, _, _, admin_group_members) = getgrnam('sudo')
return current_user in admin_group_members
def mac2eui64(mac, prefix=None):
"""
Convert a MAC address to a EUI64 address or, with prefix provided, a full
IPv6 address.
Thankfully copied from https://gist.github.com/wido/f5e32576bb57b5cc6f934e177a37a0d3
"""
import re
from ipaddress import ip_network
# http://tools.ietf.org/html/rfc4291#section-2.5.1
eui64 = re.sub(r'[.:-]', '', mac).lower()
eui64 = eui64[0:6] + 'fffe' + eui64[6:]
eui64 = hex(int(eui64[0:2], 16) ^ 2)[2:].zfill(2) + eui64[2:]
if prefix is None:
return ':'.join(re.findall(r'.{4}', eui64))
else:
try:
net = ip_network(prefix, strict=False)
euil = int('0x{0}'.format(eui64), 16)
return str(net[euil])
except: # pylint: disable=bare-except
return
-def is_bridge_member(interface):
- """
- Checks if passed interfaces is part of a bridge device or not.
-
- Returns a tuple:
- False, None -> Not part of a bridge
- True, bridge-name -> If it is assigned to a bridge
- """
- from vyos.config import Config
- c = Config()
- base = ['interfaces', 'bridge']
- for bridge in c.list_nodes(base):
- members = c.list_nodes(base + [bridge, 'member', 'interface'])
- if interface in members:
- return (True, bridge)
-
- return False, None
-
def get_half_cpus():
""" return 1/2 of the numbers of available CPUs """
cpu = os.cpu_count()
if cpu > 1:
cpu /= 2
return int(cpu)
diff --git a/python/vyos/validate.py b/python/vyos/validate.py
index 9d413ffab..15716ada4 100644
--- a/python/vyos/validate.py
+++ b/python/vyos/validate.py
@@ -1,242 +1,267 @@
# Copyright 2018 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 socket
import netifaces
import ipaddress
def is_ip(addr):
"""
Check addr if it is an IPv4 or IPv6 address
"""
return is_ipv4(addr) or is_ipv6(addr)
def is_ipv4(addr):
"""
Check addr if it is an IPv4 address/network. Returns True/False
"""
# With the below statement we can check for IPv4 networks and host
# addresses at the same time
try:
if ipaddress.ip_address(addr.split(r'/')[0]).version == 4:
return True
except:
pass
return False
def is_ipv6(addr):
"""
Check addr if it is an IPv6 address/network. Returns True/False
"""
# With the below statement we can check for IPv4 networks and host
# addresses at the same time
try:
if ipaddress.ip_network(addr.split(r'/')[0]).version == 6:
return True
except:
pass
return False
def is_ipv6_link_local(addr):
"""
Check addr if it is an IPv6 link-local address/network. Returns True/False
"""
addr = addr.split('%')[0]
if is_ipv6(addr):
if ipaddress.IPv6Address(addr).is_link_local:
return True
return False
def _are_same_ip(one, two):
# compare the binary representation of the IP
f_one = socket.AF_INET if is_ipv4(one) else socket.AF_INET6
s_two = socket.AF_INET if is_ipv4(two) else socket.AF_INET6
return socket.inet_pton(f_one, one) == socket.inet_pton(f_one, two)
def is_intf_addr_assigned(intf, addr):
if '/' in addr:
ip,mask = addr.split('/')
return _is_intf_addr_assigned(intf, ip, mask)
return _is_intf_addr_assigned(intf, addr)
def _is_intf_addr_assigned(intf, address, netmask=''):
"""
Verify if the given IPv4/IPv6 address is assigned to specific interface.
It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR
address 192.0.2.1/24.
"""
# check if the requested address type is configured at all
# {
- # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
- # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}],
+ # 17: [{'addr': '08:00:27:d9:5b:04', 'broadcast': 'ff:ff:ff:ff:ff:ff'}],
+ # 2: [{'addr': '10.0.2.15', 'netmask': '255.255.255.0', 'broadcast': '10.0.2.255'}],
# 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}]
# }
try:
ifaces = netifaces.ifaddresses(intf)
except ValueError as e:
print(e)
return False
# determine IP version (AF_INET or AF_INET6) depending on passed address
addr_type = netifaces.AF_INET if is_ipv4(address) else netifaces.AF_INET6
# Check every IP address on this interface for a match
for ip in ifaces.get(addr_type,[]):
# ip can have the interface name in the 'addr' field, we need to remove it
# {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'}
ip_addr = ip['addr'].split('%')[0]
if not _are_same_ip(address, ip_addr):
continue
# we do not have a netmask to compare against, they are the same
if netmask == '':
return True
prefixlen = ''
if is_ipv4(ip_addr):
prefixlen = sum([bin(int(_)).count('1') for _ in ip['netmask'].split('.')])
else:
prefixlen = sum([bin(int(_,16)).count('1') for _ in ip['netmask'].split(':') if _])
if str(prefixlen) == netmask:
return True
return False
def is_addr_assigned(addr):
"""
Verify if the given IPv4/IPv6 address is assigned to any interface
"""
for intf in netifaces.interfaces():
tmp = is_intf_addr_assigned(intf, addr)
if tmp == True:
return True
return False
def is_loopback_addr(addr):
"""
Check if supplied IPv4/IPv6 address is a loopback address
"""
return ipaddress.ip_address(addr).is_loopback
def is_subnet_connected(subnet, primary=False):
"""
Verify is the given IPv4/IPv6 subnet is connected to any interface on this
system.
primary check if the subnet is reachable via the primary IP address of this
interface, or in other words has a broadcast address configured. ISC DHCP
for instance will complain if it should listen on non broadcast interfaces.
Return True/False
"""
# determine IP version (AF_INET or AF_INET6) depending on passed address
addr_type = netifaces.AF_INET
if is_ipv6(subnet):
addr_type = netifaces.AF_INET6
for interface in netifaces.interfaces():
# check if the requested address type is configured at all
if addr_type not in netifaces.ifaddresses(interface).keys():
continue
# An interface can have multiple addresses, but some software components
# only support the primary address :(
if primary:
ip = netifaces.ifaddresses(interface)[addr_type][0]['addr']
if ipaddress.ip_address(ip) in ipaddress.ip_network(subnet):
return True
else:
# Check every assigned IP address if it is connected to the subnet
# in question
for ip in netifaces.ifaddresses(interface)[addr_type]:
# remove interface extension (e.g. %eth0) that gets thrown on the end of _some_ addrs
addr = ip['addr'].split('%')[0]
if ipaddress.ip_address(addr) in ipaddress.ip_network(subnet):
return True
return False
def assert_boolean(b):
if int(b) not in (0, 1):
raise ValueError(f'Value {b} out of range')
def assert_range(value, lower=0, count=3):
if int(value) not in range(lower,lower+count):
raise ValueError("Value out of range")
def assert_list(s, l):
if s not in l:
o = ' or '.join([f'"{n}"' for n in l])
raise ValueError(f'state must be {o}, got {s}')
def assert_number(n):
if not str(n).isnumeric():
raise ValueError(f'{n} must be a number')
def assert_positive(n, smaller=0):
assert_number(n)
if int(n) < smaller:
raise ValueError(f'{n} is smaller than {limit}')
def assert_mtu(mtu, min=68, max=9000):
assert_number(mtu)
if int(mtu) < min or int(mtu) > max:
raise ValueError(f'Invalid MTU size: "{mtu}"')
def assert_mac(m):
split = m.split(':')
size = len(split)
# a mac address consits out of 6 octets
if size != 6:
raise ValueError(f'wrong number of MAC octets ({size}): {m}')
octets = []
try:
for octet in split:
octets.append(int(octet, 16))
except ValueError:
raise ValueError(f'invalid hex number "{octet}" in : {m}')
# validate against the first mac address byte if it's a multicast
# address
if octets[0] & 1:
raise ValueError(f'{m} is a multicast MAC address')
# overall mac address is not allowed to be 00:00:00:00:00:00
if sum(octets) == 0:
raise ValueError('00:00:00:00:00:00 is not a valid MAC address')
if octets[:5] == (0, 0, 94, 0, 1):
raise ValueError(f'{m} is a VRRP MAC address')
+
+def is_bridge_member(conf, interface):
+ """
+ Checks if passed interfaces is part of a bridge device or not.
+
+ Returns a tuple:
+ None -> Interface not a bridge member
+ Bridge -> Interface is a member of this bridge
+ """
+ ret_val = None
+ old_level = conf.get_level()
+
+ # set config level to root
+ conf.set_level([])
+ base = ['interfaces', 'bridge']
+ for bridge in conf.list_nodes(base):
+ members = conf.list_nodes(base + [bridge, 'member', 'interface'])
+ print(members)
+ if interface in members:
+ ret_val = bridge
+ break
+
+ old_level = conf.set_level(old_level)
+ return ret_val
+
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index fd1f218d1..380457772 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -1,530 +1,530 @@
#!/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
from copy import deepcopy
from sys import exit
from netifaces import interfaces
from vyos.ifconfig import BondIf
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
from vyos.configdict import list_diff, vlan_to_dict
from vyos.config import Config
-from vyos.util import is_bridge_member
from vyos.util import call
+from vyos.validate import is_bridge_member
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_autoconf': 0,
'ipv6_eui64_prefix': '',
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
- 'intf': '',
+ '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():
# 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")
bond = 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')
bond['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if bond has been removed
cfg_base = 'interfaces bonding ' + bond['intf']
if not conf.exists(cfg_base):
bond['deleted'] = True
+ # check if interface is member if a bridge
+ bond['is_bridge_member'] = is_bridge_member(conf, bond['intf'])
return bond
# set new configuration level
conf.set_level(cfg_base)
# retrieve configured interface addresses
if conf.exists('address'):
bond['address'] = conf.return_values('address')
# get interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values('address')
bond['address_remove'] = list_diff(eff_addr, bond['address'])
# 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')
# retrieve interface description
if conf.exists('description'):
bond['description'] = conf.return_value('description')
# get DHCP client identifier
if conf.exists('dhcp-options client-id'):
bond['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'):
bond['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
# DHCP client vendor identifier
if conf.exists('dhcp-options vendor-class-id'):
bond['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'):
bond['dhcpv6_prm_only'] = True
# DHCPv6 temporary IPv6 address
if conf.exists('dhcpv6-options temporary'):
bond['dhcpv6_temporary'] = True
# ignore link state changes
if conf.exists('disable-link-detect'):
bond['disable_link_detect'] = 2
# disable bond interface
if conf.exists('disable'):
bond['disable'] = True
# 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'))
# ARP filter configuration
if conf.exists('ip disable-arp-filter'):
bond['ip_disable_arp_filter'] = 0
# ARP enable accept
if conf.exists('ip enable-arp-accept'):
bond['ip_enable_arp_accept'] = 1
# ARP enable announce
if conf.exists('ip enable-arp-announce'):
bond['ip_enable_arp_announce'] = 1
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
bond['ip_enable_arp_ignore'] = 1
# Enable proxy-arp on this interface
if conf.exists('ip enable-proxy-arp'):
bond['ip_proxy_arp'] = 1
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
bond['ip_proxy_arp_pvlan'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
bond['ipv6_autoconf'] = 1
# Get prefix for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
bond['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
bond['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
bond['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
# Media Access Control (MAC) address
if conf.exists('mac'):
bond['mac'] = conf.return_value('mac')
# 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)
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
bond['mtu'] = int(conf.return_value('mtu'))
# 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')
# retrieve VRF instance
if conf.exists('vrf'):
bond['vrf'] = conf.return_value('vrf')
# get vif-s interfaces (currently effective) - to determine which vif-s
# interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif-s')
act_intf = conf.list_nodes('vif-s')
bond['vif_s_remove'] = list_diff(eff_intf, act_intf)
if conf.exists('vif-s'):
for vif_s in conf.list_nodes('vif-s'):
# set config level to vif-s interface
conf.set_level(cfg_base + ' vif-s ' + vif_s)
bond['vif_s'].append(vlan_to_dict(conf))
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# Determine vif interfaces (currently effective) - to determine which
# vif interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif')
act_intf = conf.list_nodes('vif')
bond['vif_remove'] = list_diff(eff_intf, act_intf)
if conf.exists('vif'):
for vif in conf.list_nodes('vif'):
# set config level to vif interface
conf.set_level(cfg_base + ' vif ' + vif)
bond['vif'].append(vlan_to_dict(conf))
return bond
def verify(bond):
if bond['deleted']:
- interface = bond['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if bond['is_bridge_member']:
+ interface = bond['intf']
+ bridge = bond['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
return None
if len (bond['arp_mon_tgt']) > 16:
raise ConfigError('The maximum number of targets that can be specified 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 "{}"!'.format(bond['mode']))
vrf_name = bond['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
# 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('interface {} does not exist!'.format(intf))
# 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('can not enslave interface {} which already ' \
'belongs to {}'.format(intf, tmp))
# can not add interfaces with an assigned address to a bond
if conf.exists('interfaces ethernet ' + intf + ' address'):
raise ConfigError('can not enslave interface {} which has an address ' \
'assigned'.format(intf))
# bond members are not allowed to be bridge members, too
for tmp in conf.list_nodes('interfaces bridge'):
if conf.exists('interfaces bridge ' + tmp + ' member interface ' + intf):
raise ConfigError('can not enslave interface {} which belongs to ' \
'bridge {}'.format(intf, tmp))
# bond members are not allowed to be vrrp members, too
for tmp in conf.list_nodes('high-availability vrrp group'):
if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
raise ConfigError('can not enslave interface {} which belongs to ' \
'VRRP group {}'.format(intf, 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('can not enslave interface {} which belongs to ' \
'pseudo-ethernet {}'.format(intf, 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('can not enslave interface {} which belongs to ' \
'vxlan {}'.format(intf, tmp))
if bond['primary']:
if bond['primary'] not in bond['member']:
raise ConfigError('primary interface must be a member interface of {}' \
.format(bond['intf']))
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 address autoconfiguration
b.set_ipv6_autoconf(bond['ipv6_autoconf'])
# IPv6 EUI-based address
b.set_ipv6_eui64_address(bond['ipv6_eui64_prefix'])
# 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'])
# Change interface MAC address
if bond['mac']:
b.set_mac(bond['mac'])
# 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']:
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
b.set_vrf(bond['vrf'])
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in bond['vif_s_remove']:
b.del_vlan(vif_s)
# create service VLAN interfaces (vif-s)
for vif_s in bond['vif_s']:
s_vlan = b.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 vif_s['vif_c_remove']:
s_vlan.del_vlan(vif_c)
# create client VLAN interfaces (vif-c)
# on lower service VLAN interface
for vif_c in vif_s['vif_c']:
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 bond['vif_remove']:
b.del_vlan(vif)
# create VLAN interfaces (vif)
for vif in bond['vif']:
vlan = b.add_vlan(vif['id'])
apply_vlan_config(vlan, vif)
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-dummy.py b/src/conf_mode/interfaces-dummy.py
index a256103af..2cc60fb16 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -1,138 +1,139 @@
#!/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
from copy import deepcopy
from sys import exit
from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
-from vyos.util import is_bridge_member
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
'address': [],
'address_remove': [],
'deleted': False,
'description': '',
'disable': False,
'intf': '',
+ 'is_bridge_member': False,
'vrf': ''
}
def get_config():
dummy = 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')
dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces dummy ' + dummy['intf']):
dummy['deleted'] = True
+ # check if interface is member if a bridge
+ dummy['is_bridge_member'] = is_bridge_member(conf, dummy['intf'])
return dummy
# set new configuration level
conf.set_level('interfaces dummy ' + dummy['intf'])
# retrieve configured interface addresses
if conf.exists('address'):
dummy['address'] = conf.return_values('address')
# retrieve interface description
if conf.exists('description'):
dummy['description'] = conf.return_value('description')
# Disable this interface
if conf.exists('disable'):
dummy['disable'] = True
# Determine interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed from the interface
eff_addr = conf.return_effective_values('address')
act_addr = conf.return_values('address')
dummy['address_remove'] = list_diff(eff_addr, act_addr)
# retrieve VRF instance
if conf.exists('vrf'):
dummy['vrf'] = conf.return_value('vrf')
return dummy
def verify(dummy):
if dummy['deleted']:
- interface = dummy['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if dummy['is_bridge_member']::
+ interface = dummy['intf']
+ bridge = dummy['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
vrf_name = dummy['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
return None
def generate(dummy):
return None
def apply(dummy):
d = DummyIf(dummy['intf'])
# Remove dummy interface
if dummy['deleted']:
d.remove()
else:
# update interface description used e.g. within SNMP
d.set_alias(dummy['description'])
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in dummy['address_remove']:
d.del_addr(addr)
for addr in dummy['address']:
d.add_addr(addr)
# assign/remove VRF
d.set_vrf(dummy['vrf'])
# disable interface on demand
if dummy['disable']:
d.set_admin_state('down')
else:
d.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-geneve.py b/src/conf_mode/interfaces-geneve.py
index e47473d76..708a64474 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -1,171 +1,172 @@
#!/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
from sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import GeneveIf
-from vyos.util import is_bridge_member
+from vyos.validate import is_bridge_member
from vyos import ConfigError
default_config_data = {
'address': [],
'deleted': False,
'description': '',
'disable': False,
'intf': '',
'ip_arp_cache_tmo': 30,
'ip_proxy_arp': 0,
+ 'is_bridge_member': False,
'mtu': 1500,
'remote': '',
'vni': ''
}
def get_config():
geneve = 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')
geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists('interfaces geneve ' + geneve['intf']):
geneve['deleted'] = True
+ # check if interface is member if a bridge
+ geneve['is_bridge_member'] = is_bridge_member(conf, geneve['intf'])
return geneve
# set new configuration level
conf.set_level('interfaces geneve ' + geneve['intf'])
# retrieve configured interface addresses
if conf.exists('address'):
geneve['address'] = conf.return_values('address')
# retrieve interface description
if conf.exists('description'):
geneve['description'] = conf.return_value('description')
# Disable this interface
if conf.exists('disable'):
geneve['disable'] = True
# ARP cache entry timeout in seconds
if conf.exists('ip arp-cache-timeout'):
geneve['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
# Enable proxy-arp on this interface
if conf.exists('ip enable-proxy-arp'):
geneve['ip_proxy_arp'] = 1
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
geneve['mtu'] = int(conf.return_value('mtu'))
# Remote address of GENEVE tunnel
if conf.exists('remote'):
geneve['remote'] = conf.return_value('remote')
# Virtual Network Identifier
if conf.exists('vni'):
geneve['vni'] = conf.return_value('vni')
return geneve
def verify(geneve):
if geneve['deleted']:
- interface = geneve['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if geneve['is_bridge_member']:
+ interface = geneve['intf']
+ bridge = geneve['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not geneve['remote']:
raise ConfigError('GENEVE remote must be configured')
if not geneve['vni']:
raise ConfigError('GENEVE VNI must be configured')
return None
def generate(geneve):
return None
def apply(geneve):
# Check if GENEVE interface already exists
if geneve['intf'] in interfaces():
g = GeneveIf(geneve['intf'])
# GENEVE is super picky and the tunnel always needs to be recreated,
# thus we can simply always delete it first.
g.remove()
if not geneve['deleted']:
# GENEVE 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(GeneveIf.get_config())
# Assign GENEVE instance configuration parameters to config dict
conf['vni'] = geneve['vni']
conf['remote'] = geneve['remote']
# Finally create the new interface
g = GeneveIf(geneve['intf'], **conf)
# update interface description used e.g. by SNMP
g.set_alias(geneve['description'])
# Maximum Transfer Unit (MTU)
g.set_mtu(geneve['mtu'])
# configure ARP cache timeout in milliseconds
g.set_arp_cache_tmo(geneve['ip_arp_cache_tmo'])
# Enable proxy-arp on this interface
g.set_proxy_arp(geneve['ip_proxy_arp'])
# 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 geneve['address']:
g.add_addr(addr)
# As the GENEVE interface is always disabled first when changing
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not geneve['disable']:
g.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-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 11ba9acdd..c7c9f6aca 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -1,257 +1,258 @@
#!/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
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.util import is_bridge_member
-from netifaces import interfaces
+from vyos.validate import is_bridge_member
default_config_data = {
'address': [],
'deleted': False,
'description': '',
'disable': False,
'encapsulation': 'udp',
'local_address': '',
'local_port': 5000,
'intf': '',
'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 has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
- # to delete the l2tpv3 interface we need to current
- # tunnel_id and session_id
- if conf.exists_effective('interfaces l2tpv3 {} tunnel-id'.format(l2tpv3['intf'])):
- l2tpv3['tunnel_id'] = conf.return_effective_value(
- 'interfaces l2tpv3 {} tunnel-id'.format(l2tpv3['intf']))
+ interface = l2tpv3['intf']
+ # check if interface is member if a bridge
+ l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface)
- if conf.exists_effective('interfaces l2tpv3 {} session-id'.format(l2tpv3['intf'])):
- l2tpv3['session_id'] = conf.return_effective_value(
- 'interfaces l2tpv3 {} session-id'.format(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 prefix for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
l2tpv3['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
# 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'))
# 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']:
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if l2tpv3['is_bridge_member']:
+ interface = l2tpv3['intf']
+ bridge = l2tpv3['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not l2tpv3['local_address']:
raise ConfigError(f'Must configure the 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}')
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 address autoconfiguration
l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
# IPv6 EUI-based address
l.set_ipv6_eui64_address(l2tpv3['ipv6_eui64_prefix'])
# 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)
# 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')
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 836deb64b..9cea07a61 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -1,1054 +1,1054 @@
#!/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.ifconfig import VTunIf
-from vyos.util import call, is_bridge_member, chown, chmod_600, chmod_755
-from vyos.validate import is_addr_assigned
-from vyos import ConfigError
from vyos.template import render
-
+from vyos.util import call, chown, chmod_600, chmod_755
+from vyos.validate import is_addr_assigned, is_bridge_member
+from vyos import ConfigError
user = 'openvpn'
group = 'openvpn'
default_config_data = {
'address': [],
'auth_user': '',
'auth_pass': '',
'auth': False,
'bridge_member': [],
'compress_lzo': False,
'deleted': False,
'description': '',
'disable': False,
'disable_ncp': False,
'encryption': '',
'hash': '',
'intf': '',
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': '',
'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',
'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']
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
+ # check if interface is member if a bridge
+ openvpn['is_bridge_member'] = is_bridge_member(conf, openvpn['intf'])
return openvpn
# Check if we belong to any bridge interface
for bridge in conf.list_nodes('interfaces bridge'):
for intf in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)):
if intf == openvpn['intf']:
openvpn['bridge_member'].append(intf)
# bridged server should not have a pool by default (but can be specified manually)
if openvpn['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 prefix for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
openvpn['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
# 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'))
# 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'
# 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['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']:
- interface = openvpn['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if openvpn['is_bridge_member']:
+ interface = openvpn['intf']
+ bridge = openvpn['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
# Checks which need to be performed on interface rmeoval
if openvpn['deleted']:
# OpenVPN interface can not be deleted if it's still member of a bridge
if openvpn['bridge_member']:
raise ConfigError('Can not delete {} as it is a member interface of bridge {}!'.format(openvpn['intf'], bridge))
# 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['bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
raise ConfigError('Must specify "local-address" or "bridge member interface"')
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['bridge_member']:
raise ConfigError('Must specify "server subnet" or "bridge member interface" 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):
if openvpn['deleted'] or openvpn['disable']:
return None
interface = openvpn['intf']
directory = os.path.dirname(get_config_name(interface))
# we can't know in advance which clients have been,
# remove all client configs
ccd_dir = os.path.join(directory, 'ccd', interface)
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
# 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
user_auth_file = f'/tmp/openvpn-{interface}-pw'
if openvpn['auth']:
with open(user_auth_file, 'w') as f:
f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass']))
# also change permission on auth file
fix_permissions.append(user_auth_file)
else:
# delete old auth file if present
if os.path.isfile(user_auth_file):
os.remove(user_auth_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 file
if os.path.isfile(get_config_name(interface)):
os.remove(get_config_name(interface))
# cleanup client config dir
directory = os.path.dirname(get_config_name(interface))
ccd_dir = os.path.join(directory, 'ccd', interface)
if os.path.isdir(ccd_dir):
rmtree(ccd_dir, ignore_errors=True)
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 address autoconfiguration
o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
# IPv6 EUI-based address
o.set_ipv6_eui64_address(openvpn['ipv6_eui64_prefix'])
# 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'])
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 655006146..3e5b7be5b 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -1,387 +1,388 @@
#!/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
from copy import deepcopy
from sys import exit
from netifaces import interfaces
+from vyos.config import Config
+from vyos.configdict import list_diff, vlan_to_dict
from vyos.ifconfig import MACVLANIf
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
-from vyos.config import Config
-from vyos.util import is_bridge_member
+from vyos.validate import is_bridge_member
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_autoconf': 0,
'ipv6_eui64_prefix': '',
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
- 'intf': '',
+ '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():
peth = 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')
peth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
cfg_base = ['interfaces', 'pseudo-ethernet', peth['intf']]
# Check if interface has been removed
if not conf.exists(cfg_base):
peth['deleted'] = True
+ # check if interface is member if a bridge
+ peth['is_bridge_member'] = is_bridge_member(conf, peth['intf'])
return peth
# set new configuration level
conf.set_level(cfg_base)
# retrieve configured interface addresses
if conf.exists(['address']):
peth['address'] = conf.return_values(['address'])
# get interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values(['address'])
peth['address_remove'] = list_diff(eff_addr, peth['address'])
# retrieve interface description
if conf.exists(['description']):
peth['description'] = conf.return_value(['description'])
# get DHCP client identifier
if conf.exists(['dhcp-options', 'client-id']):
peth['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']):
peth['dhcp_hostname'] = conf.return_value(['dhcp-options', 'host-name'])
# DHCP client vendor identifier
if conf.exists(['dhcp-options', 'vendor-class-id']):
peth['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']):
peth['dhcpv6_prm_only'] = True
# DHCPv6 temporary IPv6 address
if conf.exists(['dhcpv6-options temporary']):
peth['dhcpv6_temporary'] = True
# disable interface
if conf.exists(['disable']):
peth['disable'] = True
# ignore link state changes
if conf.exists(['disable-link-detect']):
peth['disable_link_detect'] = 2
# 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']))
# ARP filter configuration
if conf.exists(['ip', 'disable-arp-filter']):
peth['ip_disable_arp_filter'] = 0
# ARP enable accept
if conf.exists(['ip', 'enable-arp-accept']):
peth['ip_enable_arp_accept'] = 1
# ARP enable announce
if conf.exists(['ip', 'enable-arp-announce']):
peth['ip_enable_arp_announce'] = 1
# ARP enable ignore
if conf.exists(['ip', 'enable-arp-ignore']):
peth['ip_enable_arp_ignore'] = 1
# Enable proxy-arp on this interface
if conf.exists(['ip', 'enable-proxy-arp']):
peth['ip_proxy_arp'] = 1
# Enable private VLAN proxy ARP on this interface
if conf.exists(['ip', 'proxy-arp-pvlan']):
peth['ip_proxy_arp_pvlan'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
peth['ipv6_autoconf'] = 1
# Get prefix for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
peth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
peth['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
peth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
# 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
# Media Access Control (MAC) address
if conf.exists(['mac']):
peth['mac'] = conf.return_value(['mac'])
# MACvlan mode
if conf.exists(['mode']):
peth['mode'] = conf.return_value(['mode'])
# retrieve VRF instance
if conf.exists('vrf'):
peth['vrf'] = conf.return_value('vrf')
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# get vif-s interfaces (currently effective) - to determine which vif-s
# interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif-s')
act_intf = conf.list_nodes('vif-s')
peth['vif_s_remove'] = list_diff(eff_intf, act_intf)
if conf.exists('vif-s'):
for vif_s in conf.list_nodes('vif-s'):
# set config level to vif-s interface
conf.set_level(cfg_base + ['vif-s', vif_s])
peth['vif_s'].append(vlan_to_dict(conf))
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# Determine vif interfaces (currently effective) - to determine which
# vif interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif')
act_intf = conf.list_nodes('vif')
peth['vif_remove'] = list_diff(eff_intf, act_intf)
if conf.exists('vif'):
for vif in conf.list_nodes('vif'):
# set config level to vif interface
conf.set_level(cfg_base + ['vif', vif])
peth['vif'].append(vlan_to_dict(conf))
return peth
def verify(peth):
if peth['deleted']:
- interface = peth['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if peth['is_bridge_member']::
+ interface = peth['intf']
+ bridge = peth['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not peth['source_interface']:
raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
if not peth['source_interface'] in interfaces():
raise ConfigError('Pseudo-ethernet source interface does not exist')
vrf_name = peth['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
# use common function to verify VLAN configuration
verify_vlan_config(peth)
return None
def generate(peth):
return None
def apply(peth):
p = ''
if peth['deleted']:
# delete interface
p = MACVLANIf(peth['intf'])
p.remove()
return None
elif peth['source_interface_changed']:
# 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.
#
# source_interface_changed also means - the interface was not present in the
# beginning and is newly created
if peth['intf'] in interfaces():
p = MACVLANIf(peth['intf'])
p.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)
else:
p = MACVLANIf(peth['intf'])
# 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 address autoconfiguration
p.set_ipv6_autoconf(peth['ipv6_autoconf'])
# IPv6 EUI-based address
p.set_ipv6_eui64_address(peth['ipv6_eui64_prefix'])
# 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
p.set_vrf(peth['vrf'])
# Change interface MAC address
if peth['mac']:
p.set_mac(peth['mac'])
# 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)
# remove no longer required service VLAN interfaces (vif-s)
for vif_s in peth['vif_s_remove']:
p.del_vlan(vif_s)
# create service VLAN interfaces (vif-s)
for vif_s in peth['vif_s']:
s_vlan = p.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 vif_s['vif_c_remove']:
s_vlan.del_vlan(vif_c)
# create client VLAN interfaces (vif-c)
# on lower service VLAN interface
for vif_c in vif_s['vif_c']:
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 peth['vif_remove']:
p.del_vlan(vif)
# create VLAN interfaces (vif)
for vif in peth['vif']:
vlan = p.add_vlan(vif['id'])
apply_vlan_config(vlan, vif)
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-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index 6639a9b0d..d238ddb57 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,270 +1,271 @@
#!/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
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.util import is_bridge_member
+from vyos.validate import is_bridge_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_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 has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
vxlan['deleted'] = True
+ # check if interface is member if a bridge
+ vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf'])
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 prefix for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
vxlan['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
# 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'))
# 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']:
- interface = vxlan['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if vxlan['is_bridge_member']:
+ interface = vxlan['intf']
+ bridge = vxlan['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
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))
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 address autoconfiguration
v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
# IPv6 EUI-based address
v.set_ipv6_eui64_address(vxlan['ipv6_eui64_prefix'])
# 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)
- # As the bond interface is always disabled first when changing
+ # 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')
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-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 8bf81c747..423700370 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -1,306 +1,308 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018-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 sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
-from vyos.util import chown, is_bridge_member, chmod_750
-from vyos.util import call
+from vyos.util import chown, chmod_750, call
+from vyos.validate import is_bridge_member
from vyos import ConfigError
kdir = r'/config/auth/wireguard'
default_config_data = {
'intfc': '',
'address': [],
'address_remove': [],
'description': '',
'lport': None,
'deleted': False,
'disable': False,
+ 'is_bridge_member': False,
'fwmark': 0x00,
'mtu': 1420,
'peer': [],
'peer_remove': [], # stores public keys of peers to remove
'pk': f'{kdir}/default/private.key',
'vrf': ''
}
def _check_kmod():
modules = ['wireguard']
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 _migrate_default_keys():
if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'):
location = f'{kdir}/default'
if not os.path.exists(location):
os.makedirs(location)
chown(location, 'root', 'vyattacfg')
chmod_750(location)
os.rename(f'{kdir}/private.key', f'{location}/private.key')
os.rename(f'{kdir}/public.key', f'{location}/public.key')
def get_config():
conf = Config()
base = ['interfaces', 'wireguard']
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
wg = deepcopy(default_config_data)
wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# Check if interface has been removed
if not conf.exists(base + [wg['intf']]):
wg['deleted'] = True
+ # check if interface is member if a bridge
+ wg['is_bridge_member'] = is_bridge_member(conf, wg['intf'])
return wg
conf.set_level(base + [wg['intf']])
# retrieve configured interface addresses
if conf.exists(['address']):
wg['address'] = conf.return_values(['address'])
# get interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values(['address'])
wg['address_remove'] = list_diff(eff_addr, wg['address'])
# retrieve interface description
if conf.exists(['description']):
wg['description'] = conf.return_value(['description'])
# disable interface
if conf.exists(['disable']):
wg['disable'] = True
# local port to listen on
if conf.exists(['port']):
wg['lport'] = conf.return_value(['port'])
# fwmark value
if conf.exists(['fwmark']):
wg['fwmark'] = int(conf.return_value(['fwmark']))
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
wg['mtu'] = int(conf.return_value(['mtu']))
# retrieve VRF instance
if conf.exists('vrf'):
wg['vrf'] = conf.return_value('vrf')
# private key
if conf.exists(['private-key']):
wg['pk'] = "{0}/{1}/private.key".format(
kdir, conf.return_value(['private-key']))
# peer removal, wg identifies peers by its pubkey
peer_eff = conf.list_effective_nodes(['peer'])
peer_rem = list_diff(peer_eff, conf.list_nodes(['peer']))
for peer in peer_rem:
wg['peer_remove'].append(
conf.return_effective_value(['peer', peer, 'pubkey']))
# peer settings
if conf.exists(['peer']):
for p in conf.list_nodes(['peer']):
# set new config level for this peer
conf.set_level(base + [wg['intf'], 'peer', p])
peer = {
'allowed-ips': [],
'address': '',
'name': p,
'persistent_keepalive': '',
'port': '',
'psk': '',
'pubkey': ''
}
# peer allowed-ips
if conf.exists(['allowed-ips']):
peer['allowed-ips'] = conf.return_values(['allowed-ips'])
# peer address
if conf.exists(['address']):
peer['address'] = conf.return_value(['address'])
# peer port
if conf.exists(['port']):
peer['port'] = conf.return_value(['port'])
# persistent-keepalive
if conf.exists(['persistent-keepalive']):
peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive'])
# preshared-key
if conf.exists(['preshared-key']):
peer['psk'] = conf.return_value(['preshared-key'])
# peer pubkeys
if conf.exists(['pubkey']):
key_eff = conf.return_effective_value(['pubkey'])
key_cfg = conf.return_value(['pubkey'])
peer['pubkey'] = key_cfg
# on a pubkey change we need to remove the pubkey first
# peers are identified by pubkey, so key update means
# peer removal and re-add
if key_eff != key_cfg and key_eff != None:
wg['peer_remove'].append(key_cfg)
# if a peer is disabled, we have to exec a remove for it's pubkey
if conf.exists(['disable']):
wg['peer_remove'].append(peer['pubkey'])
else:
wg['peer'].append(peer)
return wg
def verify(wg):
interface = wg['intf']
if wg['deleted']:
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if wg['is_bridge_member']:
+ interface = wg['intf']
+ bridge = wg['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
vrf_name = wg['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
if not os.path.exists(wg['pk']):
raise ConfigError('No keys found, generate them by executing:\n' \
'"run generate wireguard [keypair|named-keypairs]"')
if not wg['address']:
raise ConfigError(f'IP address required for interface "{interface}"!')
if not wg['peer']:
raise ConfigError(f'Peer required for interface "{interface}"!')
# run checks on individual configured WireGuard peer
for peer in wg['peer']:
peer_name = peer['name']
if not peer['allowed-ips']:
raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!')
if not peer['pubkey']:
raise ConfigError(f'Peer public-key required for peer "{peer_name}"!')
def apply(wg):
# init wg class
w = WireGuardIf(wg['intf'])
# single interface removal
if wg['deleted']:
w.remove()
return None
# Configure interface address(es)
# - not longer required addresses get removed first
# - newly addresses will be added second
for addr in wg['address_remove']:
w.del_addr(addr)
for addr in wg['address']:
w.add_addr(addr)
# Maximum Transmission Unit (MTU)
w.set_mtu(wg['mtu'])
# update interface description used e.g. within SNMP
w.set_alias(wg['description'])
# assign/remove VRF
w.set_vrf(wg['vrf'])
# remove peers
for pub_key in wg['peer_remove']:
w.remove_peer(pub_key)
# peer pubkey
# setting up the wg interface
w.config['private-key'] = c['pk']
for peer in wg['peer']:
# peer pubkey
w.config['pubkey'] = peer['pubkey']
# peer allowed-ips
w.config['allowed-ips'] = peer['allowed-ips']
# local listen port
if wg['lport']:
w.config['port'] = wg['lport']
# fwmark
if c['fwmark']:
w.config['fwmark'] = wg['fwmark']
# endpoint
if peer['address'] and peer['port']:
w.config['endpoint'] = '{}:{}'.format(
peer['address'], peer['port'])
# persistent-keepalive
if peer['persistent_keepalive']:
w.config['keepalive'] = peer['persistent_keepalive']
# maybe move it into ifconfig.py
# preshared-key - needs to be read from a file
if peer['psk']:
psk_file = '/config/auth/wireguard/psk'
with open(psk_file, 'w') as f:
f.write(peer['psk'])
w.config['psk'] = psk_file
w.update()
# Enable/Disable interface
if wg['disable']:
w.set_admin_state('down')
else:
w.set_admin_state('up')
return None
if __name__ == '__main__':
try:
_check_kmod()
_migrate_default_keys()
c = get_config()
verify(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 1964af6e1..42842f9bd 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -1,773 +1,773 @@
#!/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
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, vlan_to_dict
from vyos.ifconfig import WiFiIf
from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.util import chown, is_bridge_member, call
-from vyos import ConfigError
from vyos.template import render
-
+from vyos.util import chown, call
+from vyos.validate import is_bridge_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,
'ipv6_autoconf': 0,
'ipv6_eui64_prefix': '',
'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': [],
'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():
wifi = 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')
wifi['intf'] = os.environ['VYOS_TAGNODE_VALUE']
# check if wireless interface has been removed
cfg_base = 'interfaces wireless ' + wifi['intf']
if not conf.exists(cfg_base):
wifi['deleted'] = True
+ # check if interface is member if a bridge
+ wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf'])
# 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)
# retrieve configured interface addresses
if conf.exists('address'):
wifi['address'] = conf.return_values('address')
# get interface addresses (currently effective) - to determine which
# address is no longer valid and needs to be removed
eff_addr = conf.return_effective_values('address')
wifi['address_remove'] = list_diff(eff_addr, wifi['address'])
# 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')
# retrieve interface description
if conf.exists('description'):
wifi['description'] = conf.return_value('description')
# get DHCP client identifier
if conf.exists('dhcp-options client-id'):
wifi['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'):
wifi['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
# DHCP client vendor identifier
if conf.exists('dhcp-options vendor-class-id'):
wifi['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'):
wifi['dhcpv6_prm_only'] = conf.return_value('dhcpv6-options parameters-only')
# DHCPv6 temporary IPv6 address
if conf.exists('dhcpv6-options temporary'):
wifi['dhcpv6_temporary'] = conf.return_value('dhcpv6-options temporary')
# Disable broadcast of SSID from access-point
if conf.exists('disable-broadcast-ssid'):
wifi['disable_broadcast_ssid'] = True
# ignore link state changes on this interface
if conf.exists('disable-link-detect'):
wifi['disable_link_detect'] = 2
# 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
# ARP filter configuration
if conf.exists('ip disable-arp-filter'):
wifi['ip_disable_arp_filter'] = 0
# ARP enable accept
if conf.exists('ip enable-arp-accept'):
wifi['ip_enable_arp_accept'] = 1
# ARP enable announce
if conf.exists('ip enable-arp-announce'):
wifi['ip_enable_arp_announce'] = 1
# Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
if conf.exists('ipv6 address autoconf'):
wifi['ipv6_autoconf'] = 1
# Get prefix for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
wifi['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
# ARP enable ignore
if conf.exists('ip enable-arp-ignore'):
wifi['ip_enable_arp_ignore'] = 1
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
wifi['ipv6_forwarding'] = 0
# IPv6 Duplicate Address Detection (DAD) tries
if conf.exists('ipv6 dup-addr-detect-transmits'):
wifi['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
# Wireless physical device
if conf.exists('physical-device'):
wifi['phy'] = conf.return_value('physical-device')
# Media Access Control (MAC) address
if conf.exists('mac'):
wifi['mac'] = conf.return_value('mac')
# 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')
# retrieve VRF instance
if conf.exists('vrf'):
wifi['vrf'] = conf.return_value('vrf')
# 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
# re-set configuration level to parse new nodes
conf.set_level(cfg_base)
# Determine vif interfaces (currently effective) - to determine which
# vif interface is no longer present and needs to be removed
eff_intf = conf.list_effective_nodes('vif')
act_intf = conf.list_nodes('vif')
wifi['vif_remove'] = list_diff(eff_intf, act_intf)
if conf.exists('vif'):
for vif in conf.list_nodes('vif'):
# set config level to vif interface
conf.set_level(cfg_base + ' vif ' + vif)
wifi['vif'].append(vlan_to_dict(conf))
# disable interface
if conf.exists('disable'):
wifi['disable'] = True
# 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']:
- interface = wifi['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
- return None
+ if wifi['is_bridge_member']:
+ interface = wifi['intf']
+ bridge = wifi['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ 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']))
vrf_name = wifi['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF "{vrf_name}" does not exist')
# 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
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'])
# 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'])
# 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 address autoconfiguration
w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
# IPv6 EUI-based address
w.set_ipv6_eui64_address(wifi['ipv6_eui64_prefix'])
# 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)
# remove no longer required VLAN interfaces (vif)
for vif in wifi['vif_remove']:
w.del_vlan(vif)
# create VLAN interfaces (vif)
for vif in wifi['vif']:
# 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
w.del_vlan(vif['id'])
except:
pass
vlan = w.add_vlan(vif['id'])
apply_vlan_config(vlan, vif)
# 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)
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index da1855cd9..40bd1bbaf 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -1,215 +1,212 @@
#!/usr/bin/env python3
#
# Copyright (C) 2020 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from sys import exit
from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
-from vyos.util import chown
-from vyos.util import chmod_755
-from vyos.util import is_bridge_member
-from vyos.util import cmd
-from vyos.util import call
-from vyos import ConfigError
from vyos.template import render
-
+from vyos.util import chown, chmod_755, cmd, call
+from vyos.validate import is_bridge_member
+from vyos import ConfigError
default_config_data = {
'address': [],
'apn': '',
'chat_script': '',
'deleted': False,
'description': '',
'device': 'ttyUSB0',
'disable': False,
'disable_link_detect': 1,
'on_demand': False,
'logfile': '',
'metric': '10',
'mtu': '1500',
'name_server': True,
+ 'is_bridge_member': False,
'intf': '',
'vrf': ''
}
def check_kmod():
modules = ['option', 'usb_wwan', 'usbserial']
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():
wwan = 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')
wwan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
# Check if interface has been removed
if not conf.exists('interfaces wirelessmodem ' + wwan['intf']):
wwan['deleted'] = True
+ # check if interface is member if a bridge
+ wwan['is_bridge_member'] = is_bridge_member(conf, wwan['intf'])
return wwan
# set new configuration level
conf.set_level('interfaces wirelessmodem ' + wwan['intf'])
# get metrick for backup default route
if conf.exists(['apn']):
wwan['apn'] = conf.return_value(['apn'])
# get metrick for backup default route
if conf.exists(['backup', 'distance']):
wwan['metric'] = conf.return_value(['backup', 'distance'])
# Retrieve interface description
if conf.exists(['description']):
wwan['description'] = conf.return_value(['description'])
# System device name
if conf.exists(['device']):
wwan['device'] = conf.return_value(['device'])
# disable interface
if conf.exists('disable'):
wwan['disable'] = True
# ignore link state changes
if conf.exists('disable-link-detect'):
wwan['disable_link_detect'] = 2
# Do not use DNS servers provided by the peer
if conf.exists(['mtu']):
wwan['mtu'] = conf.return_value(['mtu'])
# Do not use DNS servers provided by the peer
if conf.exists(['no-peer-dns']):
wwan['name_server'] = False
# Access concentrator name (only connect to this concentrator)
if conf.exists(['ondemand']):
wwan['on_demand'] = True
# retrieve VRF instance
if conf.exists('vrf'):
wwan['vrf'] = conf.return_value(['vrf'])
return wwan
def verify(wwan):
if wwan['deleted']:
- interface = wwan['intf']
- is_member, bridge = is_bridge_member(interface)
- if is_member:
- # can not use a f'' formatted-string here as bridge would not get
- # expanded in the print statement
- raise ConfigError('Can not delete interface "{0}" as it ' \
- 'is a member of bridge "{1}"!'.format(interface, bridge))
+ if wwan['is_bridge_member']::
+ interface = wwan['intf']
+ bridge = wwan['is_bridge_member']
+ raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+
return None
if not wwan['apn']:
raise ConfigError(f"APN for {wwan['intf']} not configured")
# we can not use isfile() here as Linux device files are no regular files
# thus the check will return False
if not os.path.exists(f"/dev/{wwan['device']}"):
raise ConfigError(f"Device {wwan['device']} does not exist")
vrf_name = wwan['vrf']
if vrf_name and vrf_name not in interfaces():
raise ConfigError(f'VRF {vrf_name} does not exist')
return None
def generate(wwan):
# set up configuration file path variables where our templates will be
# rendered into
intf = wwan['intf']
config_wwan = f'/etc/ppp/peers/{intf}'
config_wwan_chat = wwan['chat_script']
script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{intf}'
script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{intf}'
script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{intf}'
config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up,
script_wwan_ip_up, script_wwan_ip_down]
# Ensure directories for config files exist - otherwise create them on demand
for file in config_files:
dirname = os.path.dirname(file)
if not os.path.isdir(dirname):
os.mkdir(dirname)
# Always hang-up WWAN connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
if wwan['deleted']:
# Delete PPP configuration files
for file in config_files:
if os.path.exists(file):
os.unlink(file)
else:
# Create PPP configuration files
render(config_wwan, 'wwan/peer.tmpl', wwan)
# Create PPP chat script
render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
# Create script for ip-pre-up.d
render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan)
# Create script for ip-up.d
render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan)
# Create script for ip-down.d
render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', wwan)
# make generated script file executable
chmod_755(script_wwan_pre_up)
chmod_755(script_wwan_ip_up)
chmod_755(script_wwan_ip_down)
return None
def apply(wwan):
if wwan['deleted']:
# bail out early
return None
if not wwan['disable']:
# "dial" WWAN connection
intf = wwan['intf']
cmd(f'systemctl start ppp@{intf}.service')
# make logfile owned by root / vyattacfg
chown(wwan['logfile'], 'root', 'vyattacfg')
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)
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 6, 9:29 AM (16 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4653930
Default Alt Text
(178 KB)
Attached To
Mode
rVYOSONEX vyos-1x
Attached
Detach File
Event Timeline
Log In to Comment