Page MenuHomeVyOS Platform

No OneTemporary

Size
178 KB
Referenced Files
None
Subscribers
None
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("&quot;", '"'))
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

Mime Type
text/x-diff
Expires
Fri, Nov 6, 9:29 AM (16 h, 4 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
4653930
Default Alt Text
(178 KB)

Event Timeline