summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcp_server.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py5
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py145
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py6
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py4
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py2
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py331
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py156
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py36
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py262
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py22
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py56
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py82
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py56
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py248
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py55
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py53
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py69
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py212
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py60
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py2
-rwxr-xr-xsrc/conf_mode/nat.py268
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py2
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py2
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py2
-rwxr-xr-xsrc/conf_mode/protocols_pim.py2
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py2
-rwxr-xr-xsrc/conf_mode/salt-minion.py103
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py20
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py22
-rwxr-xr-xsrc/conf_mode/system-login.py11
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py4
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py4
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py60
35 files changed, 1200 insertions, 1174 deletions
diff --git a/src/conf_mode/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
index ce0e01308..d24a46220 100755
--- a/src/conf_mode/dhcp_relay.py
+++ b/src/conf_mode/dhcp_relay.py
@@ -98,11 +98,6 @@ def generate(relay):
if not relay:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcp-relay/config.tmpl', relay)
return None
diff --git a/src/conf_mode/dhcp_server.py b/src/conf_mode/dhcp_server.py
index da01f16eb..1849ece0a 100755
--- a/src/conf_mode/dhcp_server.py
+++ b/src/conf_mode/dhcp_server.py
@@ -594,11 +594,6 @@ def generate(dhcp):
if not dhcp or dhcp['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
# Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
# we can pass to ISC DHCPd
render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
index cb5a4bbfb..ecc739063 100755
--- a/src/conf_mode/dhcpv6_relay.py
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -84,11 +84,6 @@ def generate(relay):
if relay is None:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcpv6-relay/config.tmpl', relay)
return None
diff --git a/src/conf_mode/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
index ce98e39c3..9e24ee591 100755
--- a/src/conf_mode/dhcpv6_server.py
+++ b/src/conf_mode/dhcpv6_server.py
@@ -23,7 +23,7 @@ from copy import deepcopy
from vyos.config import Config
from vyos.template import render
from vyos.util import call
-from vyos.validate import is_subnet_connected
+from vyos.validate import is_subnet_connected, is_ipv6
from vyos import ConfigError
config_file = r'/run/dhcp-server/dhcpdv6.conf'
@@ -37,24 +37,25 @@ default_config_data = {
def get_config():
dhcpv6 = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service dhcpv6-server'):
+ base = ['service', 'dhcpv6-server']
+ if not conf.exists(base):
return None
else:
- conf.set_level('service dhcpv6-server')
+ conf.set_level(base)
# Check for global disable of DHCPv6 service
- if conf.exists('disable'):
+ if conf.exists(['disable']):
dhcpv6['disabled'] = True
return dhcpv6
# Preference of this DHCPv6 server compared with others
- if conf.exists('preference'):
- dhcpv6['preference'] = conf.return_value('preference')
+ if conf.exists(['preference']):
+ dhcpv6['preference'] = conf.return_value(['preference'])
# check for multiple, shared networks served with DHCPv6 addresses
- if conf.exists('shared-network-name'):
- for network in conf.list_nodes('shared-network-name'):
- conf.set_level('service dhcpv6-server shared-network-name {0}'.format(network))
+ if conf.exists(['shared-network-name']):
+ for network in conf.list_nodes(['shared-network-name']):
+ conf.set_level(base + ['shared-network-name', network])
config = {
'name': network,
'disabled': False,
@@ -62,13 +63,13 @@ def get_config():
}
# If disabled, the shared-network configuration becomes inactive
- if conf.exists('disable'):
+ if conf.exists(['disable']):
config['disabled'] = True
# check for multiple subnet configurations in a shared network
- if conf.exists('subnet'):
- for net in conf.list_nodes('subnet'):
- conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1}'.format(network, net))
+ if conf.exists(['subnet']):
+ for net in conf.list_nodes(['subnet']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
subnet = {
'network': net,
'range6_prefix': [],
@@ -84,6 +85,7 @@ def get_config():
'nis_server': [],
'nisp_domain': '',
'nisp_server': [],
+ 'prefix_delegation': [],
'sip_address': [],
'sip_hostname': [],
'sntp_server': [],
@@ -94,25 +96,25 @@ def get_config():
# least one address range statement. The range statement gives the lowest and highest
# IP addresses in a range. All IP addresses in the range should be in the subnet in
# which the range statement is declared.
- if conf.exists('address-range prefix'):
- for prefix in conf.list_nodes('address-range prefix'):
+ if conf.exists(['address-range', 'prefix']):
+ for prefix in conf.list_nodes(['address-range', 'prefix']):
range = {
'prefix': prefix,
'temporary': False
}
# Address range will be used for temporary addresses
- if conf.exists('address-range prefix {0} temporary'.format(range['prefix'])):
+ if conf.exists(['address-range' 'prefix', prefix, 'temporary']):
range['temporary'] = True
# Append to subnet temporary range6 list
subnet['range6_prefix'].append(range)
- if conf.exists('address-range start'):
- for range in conf.list_nodes('address-range start'):
+ if conf.exists(['address-range', 'start']):
+ for range in conf.list_nodes(['address-range', 'start']):
range = {
'start': range,
- 'stop': conf.return_value('address-range start {0} stop'.format(range))
+ 'stop': conf.return_value(['address-range', 'start', range, 'stop'])
}
# Append to subnet range6 list
@@ -120,70 +122,83 @@ def get_config():
# The domain-search option specifies a 'search list' of Domain Names to be used
# by the client to locate not-fully-qualified domain names.
- if conf.exists('domain-search'):
- for domain in conf.return_values('domain-search'):
- subnet['domain_search'].append('"' + domain + '"')
+ if conf.exists(['domain-search']):
+ subnet['domain_search'] = conf.return_values(['domain-search'])
# IPv6 address valid lifetime
# (at the end the address is no longer usable by the client)
# (set to 30 days, the usual IPv6 default)
- if conf.exists('lease-time default'):
- subnet['lease_def'] = conf.return_value('lease-time default')
+ if conf.exists(['lease-time', 'default']):
+ subnet['lease_def'] = conf.return_value(['lease-time', 'default'])
# Time should be the maximum length in seconds that will be assigned to a lease.
# The only exception to this is that Dynamic BOOTP lease lengths, which are not
# specified by the client, are not limited by this maximum.
- if conf.exists('lease-time maximum'):
- subnet['lease_max'] = conf.return_value('lease-time maximum')
+ if conf.exists(['lease-time', 'maximum']):
+ subnet['lease_max'] = conf.return_value(['lease-time', 'maximum'])
# Time should be the minimum length in seconds that will be assigned to a lease
- if conf.exists('lease-time minimum'):
- subnet['lease_min'] = conf.return_value('lease-time minimum')
+ if conf.exists(['lease-time', 'minimum']):
+ subnet['lease_min'] = conf.return_value(['lease-time', 'minimum'])
# Specifies a list of Domain Name System name servers available to the client.
# Servers should be listed in order of preference.
- if conf.exists('name-server'):
- subnet['dns_server'] = conf.return_values('name-server')
+ if conf.exists(['name-server']):
+ subnet['dns_server'] = conf.return_values(['name-server'])
# Ancient NIS (Network Information Service) domain name
- if conf.exists('nis-domain'):
- subnet['nis_domain'] = conf.return_value('nis-domain')
+ if conf.exists(['nis-domain']):
+ subnet['nis_domain'] = conf.return_value(['nis-domain'])
# Ancient NIS (Network Information Service) servers
- if conf.exists('nis-server'):
- subnet['nis_server'] = conf.return_values('nis-server')
+ if conf.exists(['nis-server']):
+ subnet['nis_server'] = conf.return_values(['nis-server'])
# Ancient NIS+ (Network Information Service) domain name
- if conf.exists('nisplus-domain'):
- subnet['nisp_domain'] = conf.return_value('nisplus-domain')
+ if conf.exists(['nisplus-domain']):
+ subnet['nisp_domain'] = conf.return_value(['nisplus-domain'])
# Ancient NIS+ (Network Information Service) servers
- if conf.exists('nisplus-server'):
- subnet['nisp_server'] = conf.return_values('nisplus-server')
+ if conf.exists(['nisplus-server']):
+ subnet['nisp_server'] = conf.return_values(['nisplus-server'])
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ if conf.exists(['sip-server']):
+ for value in conf.return_values(['sip-server']):
+ if is_ipv6(value):
+ subnet['sip_address'].append(value)
+ else:
+ subnet['sip_hostname'].append(value)
+
+ # List of local SNTP servers available for the client to synchronize their clocks
+ if conf.exists(['sntp-server']):
+ subnet['sntp_server'] = conf.return_values(['sntp-server'])
# Prefix Delegation (RFC 3633)
- if conf.exists('prefix-delegation'):
- print('TODO: This option is actually not implemented right now!')
+ if conf.exists(['prefix-delegation', 'start']):
+ for address in conf.list_nodes(['prefix-delegation', 'start']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address])
+ prefix = {
+ 'start' : address,
+ 'stop' : '',
+ 'length' : ''
+ }
- # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
- if conf.exists('sip-server-address'):
- subnet['sip_address'] = conf.return_values('sip-server-address')
+ if conf.exists(['prefix-length']):
+ prefix['length'] = conf.return_value(['prefix-length'])
- # Local SIP server that is to be used for all outbound SIP requests - hostname
- if conf.exists('sip-server-name'):
- for hostname in conf.return_values('sip-server-name'):
- subnet['sip_hostname'].append('"' + hostname + '"')
+ if conf.exists(['stop']):
+ prefix['stop'] = conf.return_value(['stop'])
- # List of local SNTP servers available for the client to synchronize their clocks
- if conf.exists('sntp-server'):
- subnet['sntp_server'] = conf.return_values('sntp-server')
+ subnet['prefix_delegation'].append(prefix)
#
# Static DHCP v6 leases
#
- if conf.exists('static-mapping'):
- for mapping in conf.list_nodes('static-mapping'):
- conf.set_level('service dhcpv6-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ if conf.exists(['static-mapping']):
+ for mapping in conf.list_nodes(['static-mapping']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping])
mapping = {
'name': mapping,
'disabled': False,
@@ -192,16 +207,16 @@ def get_config():
}
# This static lease is disabled
- if conf.exists('disable'):
+ if conf.exists(['disable']):
mapping['disabled'] = True
# IPv6 address used for this DHCP client
- if conf.exists('ipv6-address'):
- mapping['ipv6_address'] = conf.return_value('ipv6-address')
+ if conf.exists(['ipv6-address']):
+ mapping['ipv6_address'] = conf.return_value(['ipv6-address'])
# This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
- if conf.exists('identifier'):
- mapping['client_identifier'] = conf.return_value('identifier')
+ if conf.exists(['identifier']):
+ mapping['client_identifier'] = conf.return_value(['identifier'])
# append static mapping configuration tu subnet list
subnet['static_mapping'].append(mapping)
@@ -209,7 +224,6 @@ def get_config():
# append subnet configuration to shared network subnet list
config['subnet'].append(subnet)
-
# append shared network configuration to config dictionary
dhcpv6['shared_network'].append(config)
@@ -282,6 +296,14 @@ def verify(dhcpv6):
else:
range6_stop.append(stop)
+ # Prefix delegation sanity checks
+ for prefix in subnet['prefix_delegation']:
+ if not prefix['stop']:
+ raise ConfigError('Stop address of delegated IPv6 prefix range must be configured')
+
+ if not prefix['length']:
+ raise ConfigError('Length of delegated IPv6 prefix must be configured')
+
# We also have prefixes that require checking
for prefix in subnet['range6_prefix']:
# If configured prefix does not match our subnet, we have to check that it's inside
@@ -335,11 +357,6 @@ def generate(dhcpv6):
if not dhcpv6 or dhcpv6['disabled']:
return None
- # Create configuration directory on demand
- dirname = os.path.dirname(config_file)
- if not os.path.isdir(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6)
return None
diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py
index 567dfa4b3..f87c198f7 100755
--- a/src/conf_mode/dns_forwarding.py
+++ b/src/conf_mode/dns_forwarding.py
@@ -152,11 +152,7 @@ def generate(dns):
if dns is None:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
- render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True)
+ render(config_file, 'dns-forwarding/recursor.conf.tmpl', dns, trim_blocks=True, user='pdns', group='pdns')
return None
def apply(dns):
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
index 038f77cf9..3386324ae 100755
--- a/src/conf_mode/dynamic_dns.py
+++ b/src/conf_mode/dynamic_dns.py
@@ -217,10 +217,6 @@ def generate(dyndns):
if dyndns['deleted']:
return None
- dirname = os.path.dirname(config_file)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
# Config file must be accessible only by its owner
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
index 11df81b1d..d691e6abd 100755
--- a/src/conf_mode/flow_accounting_conf.py
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -281,7 +281,7 @@ def verify(config):
# check if configured netflow source-ip exist in the system
if config['netflow']['source-ip']:
source_ip_presented = None
- for iface in Interface.listing():
+ for iface in Section.interfaces():
for address in Interface(iface).get_addr():
# check an IP
regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
index 380457772..bdca9d170 100755
--- a/src/conf_mode/interfaces-bonding.py
+++ b/src/conf_mode/interfaces-bonding.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -21,52 +21,30 @@ 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.ifconfig_vlan import apply_all_vlans, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
from vyos.config import Config
-from vyos.util import call
-from vyos.validate import is_bridge_member
+from vyos.util import call, cmd
+from vyos.validate import is_member, has_address_configured
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'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,
- 'is_bridge_member': False,
- 'mac': '',
'mode': '802.3ad',
'member': [],
'shutdown_required': False,
- 'mtu': 1500,
'primary': '',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
- 'vrf': ''
}
@@ -89,6 +67,13 @@ def get_bond_mode(mode):
raise ConfigError('invalid bond mode "{}"'.format(mode))
def get_config():
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
+
# initialize kernel module if not loaded
if not os.path.isfile('/sys/class/net/bonding_masters'):
import syslog
@@ -97,34 +82,18 @@ def get_config():
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']
+ cfg_base = 'interfaces bonding ' + ifname
if not conf.exists(cfg_base):
+ bond = deepcopy(default_config_data)
+ bond['intf'] = ifname
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'])
+ bond, disabled = intf_to_dict(conf, default_config_data)
# ARP link monitoring frequency in milliseconds
if conf.exists('arp-monitor interval'):
@@ -134,38 +103,6 @@ def get_config():
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')
@@ -174,50 +111,10 @@ def get_config():
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')
@@ -227,10 +124,6 @@ def get_config():
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')
@@ -247,35 +140,8 @@ def get_config():
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))
+ add_to_dict(conf, disabled, bond, 'vif', 'vif')
+ add_to_dict(conf, disabled, bond, 'vif-s', 'vif_s')
return bond
@@ -283,22 +149,38 @@ def get_config():
def verify(bond):
if bond['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{bond["intf"]}" as it is a '
+ f'member of bridge "{bond["is_bridge_member"]}"!'))
+
return None
- if len (bond['arp_mon_tgt']) > 16:
- raise ConfigError('The maximum number of targets that can be specified is 16')
+ if len(bond['arp_mon_tgt']) > 16:
+ raise ConfigError('The maximum number of arp-monitor targets is 16')
if bond['primary']:
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
- raise ConfigError('Mode dependency failed, primary not supported ' \
- 'in mode "{}"!'.format(bond['mode']))
+ raise ConfigError((
+ 'Mode dependency failed, primary not supported in mode '
+ f'"{bond["mode"]}"!'))
+
+ if ( bond['is_bridge_member']
+ and ( bond['address']
+ or bond['ipv6_eui64_prefix']
+ or bond['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{bond["intf"]}" '
+ f'as it is a member of bridge "{bond["is_bridge_member"]}"!'))
+
+ if bond['vrf']:
+ if bond['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{bond["vrf"]}" does not exist')
- vrf_name = bond['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if bond['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{bond["intf"]}" cannot be member of VRF '
+ f'"{bond["vrf"]}" and bridge {bond["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(bond)
@@ -307,51 +189,55 @@ def verify(bond):
for intf in bond['member']:
# check if member interface is "real"
if intf not in interfaces():
- raise ConfigError('interface {} does not exist!'.format(intf))
+ raise ConfigError(f'Interface {intf} does not exist!')
# a bonding member interface is only allowed to be assigned to one bond!
all_bonds = conf.list_nodes('interfaces bonding')
# We do not need to check our own bond
all_bonds.remove(bond['intf'])
for tmp in all_bonds:
- if conf.exists('interfaces bonding ' + tmp + ' member interface ' + intf):
- raise ConfigError('can not enslave interface {} which already ' \
- 'belongs to {}'.format(intf, tmp))
+ if conf.exists('interfaces bonding {tmp} member interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bond "{tmp}"!'))
# can not add interfaces with an assigned address to a bond
- if 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
+ if has_address_configured(conf, intf):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it has an address assigned!'))
+
+ # bond members are not allowed to be bridge members
+ tmp = is_member(conf, intf, 'bridge')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of bridge "{tmp}"!'))
+
+ # bond members are not allowed to be vrrp members
for tmp in conf.list_nodes('high-availability vrrp group'):
- if conf.exists('high-availability vrrp group ' + tmp + ' interface ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'VRRP group {}'.format(intf, tmp))
+ if conf.exists('high-availability vrrp group {tmp} interface {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already a member of VRRP group "{tmp}"!'))
# bond members are not allowed to be underlaying psuedo-ethernet devices
for tmp in conf.list_nodes('interfaces pseudo-ethernet'):
- if conf.exists('interfaces pseudo-ethernet ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'pseudo-ethernet {}'.format(intf, tmp))
+ if conf.exists('interfaces pseudo-ethernet {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of pseudo-ethernet "{tmp}"!'))
# bond members are not allowed to be underlaying vxlan devices
for tmp in conf.list_nodes('interfaces vxlan'):
- if conf.exists('interfaces vxlan ' + tmp + ' link ' + intf):
- raise ConfigError('can not enslave interface {} which belongs to ' \
- 'vxlan {}'.format(intf, tmp))
-
+ if conf.exists('interfaces vxlan {tmp} link {intf}'):
+ raise ConfigError((
+ f'Cannot add interface "{intf}" to bond "{bond["intf"]}", '
+ f'it is already the link of VXLAN "{tmp}"!'))
if bond['primary']:
if bond['primary'] not in bond['member']:
- raise ConfigError('primary interface must be a member interface of {}' \
- .format(bond['intf']))
+ raise ConfigError(f'Bond "{bond["intf"]}" primary interface must be a member')
if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
raise ConfigError('primary interface only works for mode active-backup, ' \
@@ -364,7 +250,6 @@ def verify(bond):
return None
-
def generate(bond):
return None
@@ -414,6 +299,9 @@ def apply(bond):
if bond['dhcpv6_temporary']:
b.dhcp.v6.options['dhcpv6_temporary'] = True
+ if bond['dhcpv6_pd']:
+ b.dhcp.v6.options['dhcpv6_pd'] = bond['dhcpv6_pd']
+
# ignore link state changes
b.set_link_detect(bond['disable_link_detect'])
# Bonding transmit hash policy
@@ -432,19 +320,27 @@ def apply(bond):
b.set_proxy_arp(bond['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
b.set_proxy_arp_pvlan(bond['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ b.set_ipv6_accept_ra(bond['ipv6_accept_ra'])
# IPv6 address autoconfiguration
b.set_ipv6_autoconf(bond['ipv6_autoconf'])
- # IPv6 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'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in bond['ipv6_eui64_prefix_remove']:
+ b.del_ipv6_eui64_address(addr)
+
# Change interface MAC address
if bond['mac']:
b.set_mac(bond['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in bond['ipv6_eui64_prefix']:
+ b.add_ipv6_eui64_address(addr)
+
# Maximum Transmission Unit (MTU)
b.set_mtu(bond['mtu'])
@@ -467,6 +363,9 @@ def apply(bond):
# Add (enslave) interfaces to bond
for intf in bond['member']:
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ cmd(f'ip addr flush dev "{intf}"')
b.add_port(intf)
# As the bond interface is always disabled first when changing
@@ -485,37 +384,17 @@ def apply(bond):
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)
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not bond['is_bridge_member']:
+ b.set_vrf(bond['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if bond['is_bridge_member']:
+ b.add_to_bridge(bond['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(b, bond)
return None
diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py
index 93c6db97e..3ff339f0f 100755
--- a/src/conf_mode/interfaces-bridge.py
+++ b/src/conf_mode/interfaces-bridge.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -20,45 +20,28 @@ from copy import deepcopy
from sys import exit
from netifaces import interfaces
-from vyos.ifconfig import BridgeIf
+from vyos.ifconfig import BridgeIf, Section
from vyos.ifconfig.stp import STP
-from vyos.configdict import list_diff
+from vyos.configdict import list_diff, interface_default_data
+from vyos.validate import is_member, has_address_configured
from vyos.config import Config
+from vyos.util import cmd, get_bridge_member_config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'aging': 300,
'arp_cache_tmo': 30,
- 'description': '',
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'forwarding_delay': 14,
'hello_time': 2,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
'igmp_querier': 0,
'intf': '',
- 'mac' : '',
'max_age': 20,
'member': [],
'member_remove': [],
'priority': 32768,
- 'stp': 0,
- 'vrf': ''
+ 'stp': 0
}
def get_config():
@@ -160,9 +143,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
bridge['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
if conf.exists('ipv6 address eui64'):
- bridge['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
+ bridge['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ bridge['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, bridge['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ bridge['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ bridge['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -176,28 +171,29 @@ def get_config():
if conf.exists('mac'):
bridge['mac'] = conf.return_value('mac')
+ # Find out if MAC has changed - if so, we need to delete all IPv6 EUI64 addresses
+ # before re-adding them
+ if ( bridge['mac'] and bridge['intf'] in Section.interfaces(section='bridge')
+ and bridge['mac'] != BridgeIf(bridge['intf'], create=False).get_mac() ):
+ bridge['ipv6_eui64_prefix_remove'] += bridge['ipv6_eui64_prefix']
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if bridge['ipv6_autoconf'] or 'dhcpv6' in bridge['address']:
+ bridge['ipv6_accept_ra'] = 2
+
# Interval at which neighbor bridges are removed
if conf.exists('max-age'):
bridge['max_age'] = int(conf.return_value('max-age'))
# Determine bridge member interface (currently configured)
for intf in conf.list_nodes('member interface'):
- # cost and priority initialized with linux defaults
- # by reading /sys/devices/virtual/net/br0/brif/eth2/{path_cost,priority}
- # after adding interface to bridge after reboot
- iface = {
- 'name': intf,
- 'cost': 100,
- 'priority': 32
- }
-
- if conf.exists('member interface {} cost'.format(intf)):
- iface['cost'] = int(conf.return_value('member interface {} cost'.format(intf)))
-
- if conf.exists('member interface {} priority'.format(intf)):
- iface['priority'] = int(conf.return_value('member interface {} priority'.format(intf)))
-
- bridge['member'].append(iface)
+ # defaults are stored in util.py (they can't be here as all interface
+ # scripts use the function)
+ memberconf = get_bridge_member_config(conf, bridge['intf'], intf)
+ if memberconf:
+ memberconf['name'] = intf
+ bridge['member'].append(memberconf)
# Determine bridge member interface (currently effective) - to determine which
# interfaces is no longer assigend to the bridge and thus can be removed
@@ -228,30 +224,40 @@ def verify(bridge):
raise ConfigError(f'VRF "{vrf_name}" does not exist')
conf = Config()
- for br in conf.list_nodes('interfaces bridge'):
- # it makes no sense to verify ourself in this case
- if br == bridge['intf']:
- continue
-
- for intf in bridge['member']:
- tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
- if intf['name'] in tmp:
- raise ConfigError('Interface "{}" belongs to bridge "{}" and can not be enslaved.'.format(intf['name'], bridge['intf']))
-
- # the interface must exist prior adding it to a bridge
for intf in bridge['member']:
+ # the interface must exist prior adding it to a bridge
if intf['name'] not in interfaces():
- raise ConfigError('Can not add non existing interface "{}" to bridge "{}"'.format(intf['name'], bridge['intf']))
+ raise ConfigError((
+ f'Cannot add nonexistent interface "{intf["name"]}" '
+ f'to bridge "{bridge["intf"]}"'))
if intf['name'] == 'lo':
raise ConfigError('Loopback interface "lo" can not be added to a bridge')
- # bridge members are not allowed to be bond members, too
- for intf in bridge['member']:
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- if intf['name'] in conf.return_values('interfaces bonding ' + bond + ' member interface'):
- raise ConfigError('Interface {} belongs to bond {}, can not add it to {}'.format(intf['name'], bond, bridge['intf']))
+ # bridge members aren't allowed to be members of another bridge
+ for br in conf.list_nodes('interfaces bridge'):
+ # it makes no sense to verify ourself in this case
+ if br == bridge['intf']:
+ continue
+
+ tmp = conf.list_nodes(f'interfaces bridge {br} member interface')
+ if intf['name'] in tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bridge "{br}"!'))
+
+ # bridge members are not allowed to be bond members
+ tmp = is_member(conf, intf['name'], 'bonding')
+ if tmp:
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it is already a member of bond "{tmp}"!'))
+
+ # bridge members must not have an assigned address
+ if has_address_configured(conf, intf['name']):
+ raise ConfigError((
+ f'Cannot add interface "{intf["name"]}" to bridge '
+ f'"{bridge["intf"]}", it has an address assigned!'))
return None
@@ -281,10 +287,10 @@ def apply(bridge):
br.set_arp_announce(bridge['ip_enable_arp_announce'])
# configure ARP ignore
br.set_arp_ignore(bridge['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ br.set_ipv6_accept_ra(bridge['ipv6_accept_ra'])
# IPv6 address autoconfiguration
br.set_ipv6_autoconf(bridge['ipv6_autoconf'])
- # IPv6 EUI-based address
- br.set_ipv6_eui64_address(bridge['ipv6_eui64_prefix'])
# IPv6 forwarding
br.set_ipv6_forwarding(bridge['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
@@ -315,12 +321,16 @@ def apply(bridge):
if bridge['dhcpv6_temporary']:
br.dhcp.v6.options['dhcpv6_temporary'] = True
+ if bridge['dhcpv6_pd']:
+ br.dhcp.v6.options['dhcpv6_pd'] = br['dhcpv6_pd']
+
# assign/remove VRF
br.set_vrf(bridge['vrf'])
- # Change interface MAC address
- if bridge['mac']:
- br.set_mac(bridge['mac'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ # (adding members to a fresh bridge changes its MAC too)
+ for addr in bridge['ipv6_eui64_prefix_remove']:
+ br.del_ipv6_eui64_address(addr)
# remove interface from bridge
for intf in bridge['member_remove']:
@@ -328,8 +338,20 @@ def apply(bridge):
# add interfaces to bridge
for member in bridge['member']:
+ # if we've come here we already verified the interface doesn't
+ # have addresses configured so just flush any remaining ones
+ cmd(f'ip addr flush dev "{member["name"]}"')
br.add_port(member['name'])
+ # Change interface MAC address
+ if bridge['mac']:
+ br.set_mac(bridge['mac'])
+
+ # Add IPv6 EUI-based addresses (must be done after adding the
+ # 1st bridge member or setting its MAC)
+ for addr in bridge['ipv6_eui64_prefix']:
+ br.add_ipv6_eui64_address(addr)
+
# up/down interface
if bridge['disable']:
br.set_admin_state('down')
@@ -347,9 +369,9 @@ def apply(bridge):
for member in bridge['member']:
i = STPBridgeIf(member['name'])
# configure ARP cache timeout
- i.set_arp_cache_tmo(bridge['arp_cache_tmo'])
+ i.set_arp_cache_tmo(member['arp_cache_tmo'])
# ignore link state changes
- i.set_link_detect(bridge['disable_link_detect'])
+ i.set_link_detect(member['disable_link_detect'])
# set bridge port path cost
i.set_path_cost(member['cost'])
# set bridge port path priority
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
index 23eaa4ecb..4a77b0c1a 100755
--- a/src/conf_mode/interfaces-dummy.py
+++ b/src/conf_mode/interfaces-dummy.py
@@ -23,7 +23,7 @@ from netifaces import interfaces
from vyos.ifconfig import DummyIf
from vyos.configdict import list_diff
from vyos.config import Config
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -47,11 +47,12 @@ def get_config():
dummy['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if we are a member of any bridge
+ dummy['is_bridge_member'] = is_member(conf, dummy['intf'], 'bridge')
+
# 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
@@ -84,15 +85,26 @@ def get_config():
def verify(dummy):
if dummy['deleted']:
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}"!')
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{dummy["is_bridge_member"]}"!'))
return None
- vrf_name = dummy['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if dummy['vrf']:
+ if dummy['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{dummy["vrf"]}" does not exist')
+
+ if dummy['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{dummy["intf"]}" cannot be member of VRF '
+ f'"{dummy["vrf"]}" and bridge "{dummy["is_bridge_member"]}" '
+ f'at the same time!'))
+
+ if dummy['is_bridge_member'] and dummy['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{dummy["intf"]}" '
+ f'as it is a member of bridge "{dummy["is_bridge_member"]}"!'))
return None
@@ -117,8 +129,10 @@ def apply(dummy):
for addr in dummy['address']:
d.add_addr(addr)
- # assign/remove VRF
- d.set_vrf(dummy['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not dummy['is_bridge_member']:
+ d.set_vrf(dummy['vrf'])
# disable interface on demand
if dummy['disable']:
diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py
index 5a977d797..f45a77a3e 100755
--- a/src/conf_mode/interfaces-ethernet.py
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -21,66 +21,49 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.ifconfig import EthernetIf
-from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config
-from vyos.configdict import list_diff, vlan_to_dict
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.validate import is_member
from vyos.config import Config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
- 'description': '',
+ **interface_default_data,
'deleted': False,
- 'dhcp_client_id': '',
- 'dhcp_hostname': '',
- 'dhcp_vendor_class_id': '',
- 'dhcpv6_prm_only': False,
- 'dhcpv6_temporary': False,
- 'disable': False,
- 'disable_link_detect': 1,
'duplex': 'auto',
'flow_control': 'on',
'hw_id': '',
'ip_arp_cache_tmo': 30,
- 'ip_disable_arp_filter': 1,
- 'ip_enable_arp_accept': 0,
- 'ip_enable_arp_announce': 0,
- 'ip_enable_arp_ignore': 0,
- 'ip_proxy_arp': 0,
'ip_proxy_arp_pvlan': 0,
- 'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
- 'ipv6_forwarding': 1,
- 'ipv6_dup_addr_detect': 1,
+ 'is_bond_member': False,
'intf': '',
- 'mac': '',
- 'mtu': 1500,
'offload_gro': 'off',
'offload_gso': 'off',
'offload_sg': 'off',
'offload_tso': 'off',
'offload_ufo': 'off',
'speed': 'auto',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
'vrf': ''
}
-def get_config():
- eth = deepcopy(default_config_data)
- conf = Config()
+def get_config():
# determine tagNode instance
if 'VYOS_TAGNODE_VALUE' not in os.environ:
raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
- eth['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# check if ethernet interface has been removed
- cfg_base = ['interfaces', 'ethernet', eth['intf']]
+ cfg_base = ['interfaces', 'ethernet', ifname]
if not conf.exists(cfg_base):
+ eth = deepcopy(default_config_data)
+ eth['intf'] = ifname
eth['deleted'] = True
# we can not bail out early as ethernet interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
@@ -90,42 +73,7 @@ def get_config():
# set new configuration level
conf.set_level(cfg_base)
- # retrieve configured interface addresses
- if conf.exists('address'):
- eth['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')
- eth['address_remove'] = list_diff(eff_addr, eth['address'])
-
- # retrieve interface description
- if conf.exists('description'):
- eth['description'] = conf.return_value('description')
-
- # get DHCP client identifier
- if conf.exists('dhcp-options client-id'):
- eth['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'):
- eth['dhcp_hostname'] = conf.return_value('dhcp-options host-name')
-
- # DHCP client vendor identifier
- if conf.exists('dhcp-options vendor-class-id'):
- eth['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'):
- eth['dhcpv6_prm_only'] = True
-
- # DHCPv6 temporary IPv6 address
- if conf.exists('dhcpv6-options temporary'):
- eth['dhcpv6_temporary'] = True
-
- # ignore link state changes
- if conf.exists('disable-link-detect'):
- eth['disable_link_detect'] = 2
+ eth, disabled = intf_to_dict(conf, default_config_data)
# disable ethernet flow control (pause frames)
if conf.exists('disable-flow-control'):
@@ -135,10 +83,6 @@ def get_config():
if conf.exists('hw-id'):
eth['hw_id'] = conf.return_value('hw-id')
- # disable interface
- if conf.exists('disable'):
- eth['disable'] = True
-
# interface duplex
if conf.exists('duplex'):
eth['duplex'] = conf.return_value('duplex')
@@ -147,53 +91,12 @@ def get_config():
if conf.exists('ip arp-cache-timeout'):
eth['ip_arp_cache_tmo'] = int(conf.return_value('ip arp-cache-timeout'))
- # ARP filter configuration
- if conf.exists('ip disable-arp-filter'):
- eth['ip_disable_arp_filter'] = 0
-
- # ARP enable accept
- if conf.exists('ip enable-arp-accept'):
- eth['ip_enable_arp_accept'] = 1
-
- # ARP enable announce
- if conf.exists('ip enable-arp-announce'):
- eth['ip_enable_arp_announce'] = 1
-
- # ARP enable ignore
- if conf.exists('ip enable-arp-ignore'):
- eth['ip_enable_arp_ignore'] = 1
-
- # Enable proxy-arp on this interface
- if conf.exists('ip enable-proxy-arp'):
- eth['ip_proxy_arp'] = 1
-
# Enable private VLAN proxy ARP on this interface
if conf.exists('ip proxy-arp-pvlan'):
eth['ip_proxy_arp_pvlan'] = 1
- # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
- if conf.exists('ipv6 address autoconf'):
- eth['ipv6_autoconf'] = 1
-
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
- if conf.exists('ipv6 address eui64'):
- eth['ipv6_eui64_prefix'] = conf.return_value('ipv6 address eui64')
-
- # Disable IPv6 forwarding on this interface
- if conf.exists('ipv6 disable-forwarding'):
- eth['ipv6_forwarding'] = 0
-
- # IPv6 Duplicate Address Detection (DAD) tries
- if conf.exists('ipv6 dup-addr-detect-transmits'):
- eth['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
-
- # Media Access Control (MAC) address
- if conf.exists('mac'):
- eth['mac'] = conf.return_value('mac')
-
- # Maximum Transmission Unit (MTU)
- if conf.exists('mtu'):
- eth['mtu'] = int(conf.return_value('mtu'))
+ # check if we are a member of any bond
+ eth['is_bond_member'] = is_member(conf, eth['intf'], 'bonding')
# GRO (generic receive offload)
if conf.exists('offload-options generic-receive'):
@@ -219,37 +122,13 @@ def get_config():
if conf.exists('speed'):
eth['speed'] = conf.return_value('speed')
- # retrieve VRF instance
- if conf.exists('vrf'):
- eth['vrf'] = conf.return_value('vrf')
+ # remove default IPv6 link-local address if member of a bond
+ if eth['is_bond_member'] and 'fe80::/64' in eth['ipv6_eui64_prefix']:
+ eth['ipv6_eui64_prefix'].remove('fe80::/64')
+ eth['ipv6_eui64_prefix_remove'].append('fe80::/64')
- # 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')
- eth['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])
- eth['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')
- eth['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])
- eth['vif'].append(vlan_to_dict(conf))
+ add_to_dict(conf, disabled, eth, 'vif', 'vif')
+ add_to_dict(conf, disabled, eth, 'vif-s', 'vif_s')
return eth
@@ -272,18 +151,24 @@ def verify(eth):
if eth['dhcpv6_prm_only'] and eth['dhcpv6_temporary']:
raise ConfigError('DHCPv6 temporary and parameters-only options are mutually exclusive!')
- vrf_name = eth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ memberof = eth['is_bridge_member'] if eth['is_bridge_member'] else eth['is_bond_member']
- conf = Config()
- # some options can not be changed when interface is enslaved to a bond
- for bond in conf.list_nodes('interfaces bonding'):
- if conf.exists('interfaces bonding ' + bond + ' member interface'):
- bond_member = conf.return_values('interfaces bonding ' + bond + ' member interface')
- if eth['intf'] in bond_member:
- if eth['address']:
- raise ConfigError(f"Can not assign address to interface {eth['intf']} which is a member of {bond}")
+ if ( memberof
+ and ( eth['address']
+ or eth['ipv6_eui64_prefix']
+ or eth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{eth["intf"]}" '
+ f'as it is a member of "{memberof}"!'))
+
+ if eth['vrf']:
+ if eth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{eth["vrf"]}" does not exist')
+
+ if memberof:
+ raise ConfigError((
+ f'Interface "{eth["intf"]}" cannot be member of VRF "{eth["vrf"]}" '
+ f'and "{memberof}" at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(eth)
@@ -316,6 +201,9 @@ def apply(eth):
if eth['dhcpv6_temporary']:
e.dhcp.v6.options['dhcpv6_temporary'] = True
+ if eth['dhcpv6_pd']:
+ e.dhcp.v6.options['dhcpv6_pd'] = e['dhcpv6_pd']
+
# ignore link state changes
e.set_link_detect(eth['disable_link_detect'])
# disable ethernet flow control (pause frames)
@@ -334,15 +222,19 @@ def apply(eth):
e.set_proxy_arp(eth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
e.set_proxy_arp_pvlan(eth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ e.set_ipv6_accept_ra(eth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
e.set_ipv6_autoconf(eth['ipv6_autoconf'])
- # IPv6 EUI-based address
- e.set_ipv6_eui64_address(eth['ipv6_eui64_prefix'])
# IPv6 forwarding
e.set_ipv6_forwarding(eth['ipv6_forwarding'])
# IPv6 Duplicate Address Detection (DAD) tries
e.set_ipv6_dad_messages(eth['ipv6_dup_addr_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in eth['ipv6_eui64_prefix_remove']:
+ e.del_ipv6_eui64_address(addr)
+
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if eth['mac']:
@@ -350,6 +242,10 @@ def apply(eth):
elif eth['hw_id']:
e.set_mac(eth['hw_id'])
+ # Add IPv6 EUI-based addresses
+ for addr in eth['ipv6_eui64_prefix']:
+ e.add_ipv6_eui64_address(addr)
+
# Maximum Transmission Unit (MTU)
e.set_mtu(eth['mtu'])
@@ -385,47 +281,17 @@ def apply(eth):
for addr in eth['address']:
e.add_addr(addr)
- # assign/remove VRF
- e.set_vrf(eth['vrf'])
-
- # remove no longer required service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s_remove']:
- e.del_vlan(vif_s)
-
- # create service VLAN interfaces (vif-s)
- for vif_s in eth['vif_s']:
- s_vlan = e.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 eth['vif_remove']:
- e.del_vlan(vif)
-
- # create VLAN interfaces (vif)
- for vif in eth['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
- e.del_vlan(vif['id'])
- except:
- pass
-
- vlan = e.add_vlan(vif['id'], ingress_qos=vif['ingress_qos'], egress_qos=vif['egress_qos'])
- apply_vlan_config(vlan, vif)
+ # assign/remove VRF (ONLY when not a member of a bridge or bond,
+ # otherwise 'nomaster' removes it from it)
+ if not ( eth['is_bridge_member'] or eth['is_bond_member'] ):
+ e.set_vrf(eth['vrf'])
+
+ # re-add ourselves to any bridge we might have fallen out of
+ if eth['is_bridge_member']:
+ e.add_to_bridge(eth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(e, eth)
return None
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
index 708a64474..e4109a221 100755
--- a/src/conf_mode/interfaces-geneve.py
+++ b/src/conf_mode/interfaces-geneve.py
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import GeneveIf
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -49,11 +49,12 @@ def get_config():
geneve['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ geneve['is_bridge_member'] = is_member(conf, geneve['intf'], 'bridge')
+
# 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
@@ -97,12 +98,17 @@ def get_config():
def verify(geneve):
if geneve['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{geneve["intf"]}" as it is a '
+ f'member of bridge "{geneve["is_bridge_member"]}"!'))
return None
+ if geneve['is_bridge_member'] and geneve['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{geneve["intf"]}" '
+ f'as it is a member of bridge "{geneve["is_bridge_member"]}"!'))
+
if not geneve['remote']:
raise ConfigError('GENEVE remote must be configured')
@@ -158,6 +164,10 @@ def apply(geneve):
if not geneve['disable']:
g.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if geneve['is_bridge_member']:
+ g.add_to_bridge(geneve['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
index 8312d6f37..cdfc6ea84 100755
--- a/src/conf_mode/interfaces-l2tpv3.py
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -24,7 +24,7 @@ from vyos.config import Config
from vyos.ifconfig import L2TPv3If, Interface
from vyos import ConfigError
from vyos.util import call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, is_addr_assigned
default_config_data = {
'address': [],
@@ -35,8 +35,9 @@ default_config_data = {
'local_address': '',
'local_port': 5000,
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
@@ -66,12 +67,13 @@ def get_config():
l2tpv3['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member of a bridge
+ l2tpv3['is_bridge_member'] = is_member(conf, l2tpv3['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces l2tpv3 ' + l2tpv3['intf']):
l2tpv3['deleted'] = True
interface = l2tpv3['intf']
- # check if interface is member if a bridge
- l2tpv3['is_bridge_member'] = is_bridge_member(conf, interface)
# to delete the l2tpv3 interface we need the current tunnel_id and session_id
if conf.exists_effective(f'interfaces l2tpv3 {interface} tunnel-id'):
@@ -113,9 +115,15 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
l2tpv3['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes 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')
+ l2tpv3['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Remove the default link-local address if set.
+ if not ( conf.exists('ipv6 address no-default-link-local') or
+ l2tpv3['is_bridge_member'] ):
+ # add the link-local by default to make IPv6 work
+ l2tpv3['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -125,6 +133,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
l2tpv3['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if l2tpv3['ipv6_autoconf'] or 'dhcpv6' in l2tpv3['address']:
+ l2tpv3['ipv6_accept_ra'] = 2
+
# Maximum Transmission Unit (MTU)
if conf.exists('mtu'):
l2tpv3['mtu'] = int(conf.return_value('mtu'))
@@ -161,15 +174,18 @@ def verify(l2tpv3):
if l2tpv3['deleted']:
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}"!')
+ raise ConfigError((
+ f'Interface "{l2tpv3["intf"]}" cannot be deleted as it is a '
+ f'member of bridge "{l2tpv3["is_bridge_member"]}"!'))
return None
if not l2tpv3['local_address']:
raise ConfigError(f'Must configure the l2tpv3 local-ip for {interface}')
+ if not is_addr_assigned(l2tpv3['local_address']):
+ raise ConfigError(f'Must use a configured IP on l2tpv3 local-ip for {interface}')
+
if not l2tpv3['remote_address']:
raise ConfigError(f'Must configure the l2tpv3 remote-ip for {interface}')
@@ -185,6 +201,14 @@ def verify(l2tpv3):
if not l2tpv3['peer_session_id']:
raise ConfigError(f'Must configure the l2tpv3 peer-session-id for {interface}')
+ if ( l2tpv3['is_bridge_member']
+ and ( l2tpv3['address']
+ or l2tpv3['ipv6_eui64_prefix']
+ or l2tpv3['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{l2tpv3["intf"]}" '
+ f'as it is a member of bridge "{l2tpv3["is_bridge_member"]}"!'))
+
return None
@@ -223,10 +247,10 @@ def apply(l2tpv3):
l.set_alias(l2tpv3['description'])
# Maximum Transfer Unit (MTU)
l.set_mtu(l2tpv3['mtu'])
+ # IPv6 accept RA
+ l.set_ipv6_accept_ra(l2tpv3['ipv6_accept_ra'])
# IPv6 address autoconfiguration
l.set_ipv6_autoconf(l2tpv3['ipv6_autoconf'])
- # IPv6 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
@@ -238,12 +262,20 @@ def apply(l2tpv3):
for addr in l2tpv3['address']:
l.add_addr(addr)
+ # IPv6 EUI-based addresses
+ for addr in l2tpv3['ipv6_eui64_prefix']:
+ l.add_ipv6_eui64_address(addr)
+
# As the interface is always disabled first when changing parameters
# we will only re-enable the interface if it is not administratively
# disabled
if not l2tpv3['disable']:
l.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if l2tpv3['is_bridge_member']:
+ l.add_to_bridge(l2tpv3['is_bridge_member'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py
index 708ac8f91..ea8e1a7c4 100755
--- a/src/conf_mode/interfaces-openvpn.py
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -25,10 +25,11 @@ from time import sleep
from shutil import rmtree
from vyos.config import Config
+from vyos.configdict import list_diff
from vyos.ifconfig import VTunIf
from vyos.template import render
from vyos.util import call, chown, chmod_600, chmod_755
-from vyos.validate import is_addr_assigned, is_bridge_member, is_ipv4
+from vyos.validate import is_addr_assigned, is_member, is_ipv4
from vyos import ConfigError
user = 'openvpn'
@@ -40,7 +41,6 @@ default_config_data = {
'auth_pass': '',
'auth_user_pass_file': '',
'auth': False,
- 'bridge_member': [],
'compress_lzo': False,
'deleted': False,
'description': '',
@@ -49,8 +49,10 @@ default_config_data = {
'encryption': '',
'hash': '',
'intf': '',
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'ipv6_local_address': [],
@@ -197,21 +199,16 @@ def get_config():
openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
+ # check if interface is member of a bridge
+ openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
+
# Check if interface instance has been removed
if not conf.exists('interfaces openvpn ' + openvpn['intf']):
openvpn['deleted'] = True
- # 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']:
+ if openvpn['is_bridge_member']:
openvpn['server_pool'] = False
openvpn['server_ipv6_pool'] = False
@@ -314,9 +311,21 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
openvpn['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes 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')
+ openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ openvpn['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -326,6 +335,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
+ openvpn['ipv6_accept_ra'] = 2
+
# OpenVPN operation mode
if conf.exists('mode'):
openvpn['mode'] = conf.return_value('mode')
@@ -583,7 +597,7 @@ def get_config():
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 openvpn['server_pool'] and not openvpn['is_bridge_member']:
if not openvpn['server_pool_start']:
openvpn['server_pool_start'] = default_server['pool_start']
@@ -621,22 +635,15 @@ def get_config():
def verify(openvpn):
if openvpn['deleted']:
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}"!')
-
+ raise ConfigError((
+ f'Cannot delete interface "{openvpn["intf"]}" as it is a '
+ f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
return None
if not openvpn['mode']:
raise ConfigError('Must specify OpenVPN operation mode')
- # 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"')
@@ -666,9 +673,9 @@ def verify(openvpn):
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 openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
- raise ConfigError('Must specify "local-address" or "bridge member interface"')
+ raise ConfigError('Must specify "local-address" or add interface to bridge')
if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
@@ -747,8 +754,8 @@ def verify(openvpn):
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 not openvpn['is_bridge_member']:
+ raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
if openvpn['server_pool']:
if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
@@ -1041,15 +1048,28 @@ def apply(openvpn):
o = VTunIf(interface)
# update interface description used e.g. within SNMP
o.set_alias(openvpn['description'])
+ # IPv6 accept RA
+ o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
# IPv6 address autoconfiguration
o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
- # IPv6 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'])
+ # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
+ # If MAC has changed, old EUI64 addresses won't get deleted,
+ # but this isn't easy to solve, so leave them.
+ # This is even more difficult as openvpn uses a random MAC for the
+ # initial interface creation, unless set by 'lladdr'.
+ # NOTE: right now the interface is always deleted. For future
+ # compatibility when tap's are not deleted, leave the del_ in
+ if openvpn['mode'] == 'tap':
+ for addr in openvpn['ipv6_eui64_prefix_remove']:
+ o.del_ipv6_eui64_address(addr)
+ for addr in openvpn['ipv6_eui64_prefix']:
+ o.add_ipv6_eui64_address(addr)
+
except:
pass
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
index f942b7d2f..e46d52d19 100755
--- a/src/conf_mode/interfaces-pppoe.py
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -36,6 +36,7 @@ default_config_data = {
'deleted': False,
'description': '\0',
'disable': False,
+ 'dhcpv6_pd': [],
'intf': '',
'idle_timeout': '',
'ipv6_autoconf': False,
@@ -138,6 +139,27 @@ def get_config():
if conf.exists('vrf'):
pppoe['vrf'] = conf.return_value(['vrf'])
+ if conf.exists(['dhcpv6-options', 'delegate']):
+ for interface in conf.list_nodes(['dhcpv6-options', 'delegate']):
+ pd = {
+ 'ifname': interface,
+ 'sla_id': '',
+ 'sla_len': '',
+ 'if_id': ''
+ }
+ conf.set_level(base_path + [pppoe['intf'], 'dhcpv6-options', 'delegate', interface])
+
+ if conf.exists(['sla-id']):
+ pd['sla_id'] = conf.return_value(['sla-id'])
+
+ if conf.exists(['sla-len']):
+ pd['sla_len'] = conf.return_value(['sla-len'])
+
+ if conf.exists(['interface-id']):
+ pd['if_id'] = conf.return_value(['interface-id'])
+
+ pppoe['dhcpv6_pd'].append(pd)
+
return pppoe
def verify(pppoe):
@@ -169,15 +191,15 @@ def generate(pppoe):
script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{intf}'
script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{intf}'
script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{intf}'
+ config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{intf}.conf'
config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up,
- script_pppoe_ip_down, script_pppoe_ipv6_up]
+ script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c]
+
+ # Shutdown DHCPv6 prefix delegation client
+ if not pppoe['dhcpv6_pd']:
+ cmd(f'systemctl stop dhcp6c@{intf}.service')
- # 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 PPPoE connection prior generating new configuration file
cmd(f'systemctl stop ppp@{intf}.service')
@@ -189,27 +211,29 @@ def generate(pppoe):
os.unlink(file)
else:
+ # generated script must be executable
+
# Create PPP configuration files
render(config_pppoe, 'pppoe/peer.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-pre-up.d
render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-up.d
render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ip-down.d
render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
# Create script for ipv6-up.d
render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl',
- pppoe, trim_blocks=True)
+ pppoe, trim_blocks=True, permission=0o755)
- # make generated script file executable
- chmod_755(script_pppoe_pre_up)
- chmod_755(script_pppoe_ip_up)
- chmod_755(script_pppoe_ip_down)
- chmod_755(script_pppoe_ipv6_up)
+ if len(pppoe['dhcpv6_pd']) > 0:
+ # ipv6.tmpl relies on ifname - this should be made consitent in the
+ # future better then double key-ing the same value
+ pppoe['ifname'] = intf
+ render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True)
return None
diff --git a/src/conf_mode/interfaces-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
index d5f308ed3..3e036a753 100755
--- a/src/conf_mode/interfaces-pseudo-ethernet.py
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -21,154 +21,55 @@ 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.validate import is_bridge_member
+from vyos.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.ifconfig import MACVLANIf, Section
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
- 'description': '',
+ **interface_default_data,
'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,
- 'is_bridge_member': False,
'source_interface': '',
'source_interface_changed': False,
- 'mac': '',
'mode': 'private',
- 'vif_s': [],
+ 'vif_s': {},
'vif_s_remove': [],
- 'vif': [],
+ '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']]
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# Check if interface has been removed
+ cfg_base = ['interfaces', 'pseudo-ethernet', ifname]
if not conf.exists(cfg_base):
+ peth = deepcopy(default_config_data)
peth['deleted'] = True
- # 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
+ peth, disabled = intf_to_dict(conf, default_config_data)
# ARP cache entry timeout in seconds
if conf.exists(['ip', 'arp-cache-timeout']):
peth['ip_arp_cache_tmo'] = int(conf.return_value(['ip', 'arp-cache-timeout']))
- # 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'])
@@ -176,67 +77,49 @@ def get_config():
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))
-
+ add_to_dict(conf, disabled, peth, 'vif', 'vif')
+ add_to_dict(conf, disabled, peth, 'vif-s', 'vif_s')
return peth
def verify(peth):
if peth['deleted']:
if peth['is_bridge_member']:
- interface = peth['intf']
- bridge = peth['is_bridge_member']
- raise ConfigError(f'Interface "{interface}" can not be deleted as it belongs to bridge "{bridge}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{peth["intf"]}" as it is a '
+ f'member of bridge "{peth["is_bridge_member"]}"!'))
return None
if not peth['source_interface']:
- raise ConfigError('Link device must be set for virtual ethernet {}'.format(peth['intf']))
+ raise ConfigError((
+ f'Link device must be set for pseudo-ethernet "{peth["intf"]}"'))
if not peth['source_interface'] in interfaces():
- raise ConfigError('Pseudo-ethernet source interface does not exist')
+ raise ConfigError((
+ f'Pseudo-ethernet "{peth["intf"]}" link device does not exist'))
+
+ if ( peth['is_bridge_member']
+ and ( peth['address']
+ or peth['ipv6_eui64_prefix']
+ or peth['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{peth["intf"]}" '
+ f'as it is a member of bridge "{peth["is_bridge_member"]}"!'))
- vrf_name = peth['vrf']
- if vrf_name and vrf_name not in interfaces():
- raise ConfigError(f'VRF "{vrf_name}" does not exist')
+ if peth['vrf']:
+ if peth['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{peth["vrf"]}" does not exist')
+
+ if peth['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{peth["intf"]}" cannot be member of VRF '
+ f'"{peth["vrf"]}" and bridge {peth["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(peth)
@@ -288,6 +171,9 @@ def apply(peth):
if peth['dhcpv6_temporary']:
p.dhcp.v6.options['dhcpv6_temporary'] = True
+ if peth['dhcpv6_pd']:
+ p.dhcp.v6.options['dhcpv6_pd'] = peth['dhcpv6_pd']
+
# ignore link state changes
p.set_link_detect(peth['disable_link_detect'])
# configure ARP cache timeout in milliseconds
@@ -304,22 +190,32 @@ def apply(peth):
p.set_proxy_arp(peth['ip_proxy_arp'])
# Enable private VLAN proxy ARP on this interface
p.set_proxy_arp_pvlan(peth['ip_proxy_arp_pvlan'])
+ # IPv6 accept RA
+ p.set_ipv6_accept_ra(peth['ipv6_accept_ra'])
# IPv6 address autoconfiguration
p.set_ipv6_autoconf(peth['ipv6_autoconf'])
- # IPv6 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'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not peth['is_bridge_member']:
+ p.set_vrf(peth['vrf'])
+
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in peth['ipv6_eui64_prefix_remove']:
+ p.del_ipv6_eui64_address(addr)
# Change interface MAC address
if peth['mac']:
p.set_mac(peth['mac'])
+ # Add IPv6 EUI-based addresses
+ for addr in peth['ipv6_eui64_prefix']:
+ p.add_ipv6_eui64_address(addr)
+
# Change interface mode
p.set_mode(peth['mode'])
@@ -337,34 +233,12 @@ def apply(peth):
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)
+ # re-add ourselves to any bridge we might have fallen out of
+ if peth['is_bridge_member']:
+ p.add_to_bridge(peth['is_bridge_member'])
+
+ # apply all vlans to interface
+ apply_all_vlans(p, peth)
return None
diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py
index 9c0c42414..8e9bb069e 100755
--- a/src/conf_mode/interfaces-tunnel.py
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
from vyos.ifconfig.afi import IP4, IP6
from vyos.configdict import list_diff
-from vyos.validate import is_ipv4, is_ipv6, is_bridge_member
+from vyos.validate import is_ipv4, is_ipv6, is_member
from vyos import ConfigError
from vyos.dicts import FixedDict
@@ -222,7 +222,7 @@ class ConfigurationState(Config):
remove all the values which were not changed from the default
"""
for option in options:
- if self.exists(option) and self_return_value(option) != self.default[option]:
+ if self.exists(option) and self.self_return_value(option) != self.default[option]:
continue
del self.options[option]
@@ -251,6 +251,7 @@ default_config_data = {
'ip': False,
'ipv6': False,
'nhrp': [],
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
'ipv6_forwarding': 1,
'ipv6_dad_transmits': 1,
@@ -401,6 +402,11 @@ def get_config():
eff_addr = conf.return_effective_values('address')
options['addresses-del'] = list_diff(eff_addr, options['addresses-add'])
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']:
+ options['ipv6_accept_ra'] = 2
+
# allmulticast fate is linked to multicast
options['allmulticast'] = options['multicast']
@@ -410,7 +416,7 @@ def get_config():
options['tunnel'] = {}
# check for bridges
- options['bridge'] = is_bridge_member(conf, ifname)
+ options['bridge'] = is_member(conf, ifname, 'bridge')
options['interfaces'] = interfaces()
for name in ct:
@@ -436,11 +442,14 @@ def verify(conf):
if changes['section'] == 'delete':
if ifname in options['nhrp']:
- raise ConfigError(f'Can not delete interface tunnel {iftype} {ifname}, it is used by nhrp')
+ raise ConfigError((
+ f'Cannot delete interface tunnel {iftype} {ifname}, '
+ 'it is used by NHRP'))
- bridge = options['bridge']
- if bridge:
- raise ConfigError(f'Interface "{ifname}" can not be deleted as it belongs to bridge "{bridge}"!')
+ if options['bridge']:
+ raise ConfigError((
+ f'Cannot delete interface "{options["ifname"]}" as it is a '
+ f'member of bridge "{options["bridge"]}"!'))
# done, bail out early
return None
@@ -461,7 +470,7 @@ def verify(conf):
# what are the tunnel options we can set / modified / deleted
kls = get_class(options)
- valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf']
+ valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state']
if changes['section'] == 'create':
valid.extend(['type',])
@@ -525,15 +534,28 @@ def verify(conf):
print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
# vrf check
-
- vrf = options['vrf']
- if vrf and vrf not in options['interfaces']:
- raise ConfigError(f'VRF "{vrf}" does not exist')
+ if options['vrf']:
+ if options['vrf'] not in options['interfaces']:
+ raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Interface "{options["ifname"]}" cannot be member of VRF '
+ f'"{options["vrf"]}" and bridge {options["bridge"]} '
+ f'at the same time!'))
+
+ # bridge and address check
+ if ( options['bridge']
+ and ( options['addresses-add']
+ or options['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{options["name"]}" '
+ f'as it is a member of bridge "{options["bridge"]}"!'))
# source-interface check
if tun_dev and tun_dev not in options['interfaces']:
- raise ConfigError(f'device "{dev}" does not exist')
+ raise ConfigError(f'device "{tun_dev}" does not exist')
# tunnel encapsulation check
@@ -620,12 +642,17 @@ def apply(conf):
# set other interface properties
for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
- 'vrf', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
if not options[option]:
# should never happen but better safe
continue
tunnel.set_interface(option, options[option])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not options['bridge']:
+ tunnel.set_vrf(options['vrf'])
+
# Configure interface address(es)
for addr in options['addresses-del']:
tunnel.del_addr(addr)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
index d238ddb57..84fe3dfc8 100755
--- a/src/conf_mode/interfaces-vxlan.py
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -22,7 +22,7 @@ from netifaces import interfaces
from vyos.config import Config
from vyos.ifconfig import VXLANIf, Interface
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -38,8 +38,9 @@ default_config_data = {
'ip_enable_arp_announce': 0,
'ip_enable_arp_ignore': 0,
'ip_proxy_arp': 0,
+ 'ipv6_accept_ra': 1,
'ipv6_autoconf': 0,
- 'ipv6_eui64_prefix': '',
+ 'ipv6_eui64_prefix': [],
'ipv6_forwarding': 1,
'ipv6_dup_addr_detect': 1,
'is_bridge_member': False,
@@ -62,11 +63,12 @@ def get_config():
vxlan['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ vxlan['is_bridge_member'] = is_member(conf, vxlan['intf'], 'bridge')
+
# Check if interface has been removed
if not conf.exists('interfaces vxlan ' + vxlan['intf']):
vxlan['deleted'] = True
- # check if interface is member if a bridge
- vxlan['is_bridge_member'] = is_bridge_member(conf, vxlan['intf'])
return vxlan
# set new configuration level
@@ -116,9 +118,15 @@ def get_config():
if conf.exists('ipv6 address autoconf'):
vxlan['ipv6_autoconf'] = 1
- # Get prefix for IPv6 addressing based on MAC address (EUI-64)
+ # Get prefixes 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')
+ vxlan['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Remove the default link-local address if set.
+ if not ( conf.exists('ipv6 address no-default-link-local')
+ or vxlan['is_bridge_member'] ):
+ # add the link-local by default to make IPv6 work
+ vxlan['ipv6_eui64_prefix'].append('fe80::/64')
# Disable IPv6 forwarding on this interface
if conf.exists('ipv6 disable-forwarding'):
@@ -128,6 +136,11 @@ def get_config():
if conf.exists('ipv6 dup-addr-detect-transmits'):
vxlan['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if vxlan['ipv6_autoconf'] or 'dhcpv6' in vxlan['address']:
+ vxlan['ipv6_accept_ra'] = 2
+
# VXLAN source address
if conf.exists('source-address'):
vxlan['source_address'] = conf.return_value('source-address')
@@ -158,9 +171,9 @@ def get_config():
def verify(vxlan):
if vxlan['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{vxlan["intf"]}" as it is a '
+ f'member of bridge "{vxlan["is_bridge_member"]}"!'))
return None
@@ -188,6 +201,14 @@ def verify(vxlan):
raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
'MTU is to small ({})'.format(underlay_mtu))
+ if ( vxlan['is_bridge_member']
+ and ( vxlan['address']
+ or vxlan['ipv6_eui64_prefix']
+ or vxlan['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{vxlan["intf"]}" '
+ f'as it is a member of bridge "{vxlan["is_bridge_member"]}"!'))
+
return None
@@ -236,10 +257,10 @@ def apply(vxlan):
v.set_arp_ignore(vxlan['ip_enable_arp_ignore'])
# Enable proxy-arp on this interface
v.set_proxy_arp(vxlan['ip_proxy_arp'])
+ # IPv6 accept RA
+ v.set_ipv6_accept_ra(vxlan['ipv6_accept_ra'])
# IPv6 address autoconfiguration
v.set_ipv6_autoconf(vxlan['ipv6_autoconf'])
- # IPv6 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
@@ -251,12 +272,20 @@ def apply(vxlan):
for addr in vxlan['address']:
v.add_addr(addr)
+ # IPv6 EUI-based addresses
+ for addr in vxlan['ipv6_eui64_prefix']:
+ v.add_ipv6_eui64_address(addr)
+
# As the VXLAN interface is always disabled first when changing
# parameters we will only re-enable the interface if it is not
# administratively disabled
if not vxlan['disable']:
v.set_admin_state('up')
+ # re-add ourselves to any bridge we might have fallen out of
+ if vxlan['is_bridge_member']:
+ v.add_to_bridge(vxlan['is_bridge_member'])
+
return None
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
index 423700370..97dcf626b 100755
--- a/src/conf_mode/interfaces-wireguard.py
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -25,7 +25,7 @@ from vyos.config import Config
from vyos.configdict import list_diff
from vyos.ifconfig import WireGuardIf
from vyos.util import chown, chmod_750, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member, is_ipv6
from vyos import ConfigError
kdir = r'/config/auth/wireguard'
@@ -35,11 +35,11 @@ default_config_data = {
'address': [],
'address_remove': [],
'description': '',
- 'lport': None,
+ 'listen_port': '',
'deleted': False,
'disable': False,
+ 'fwmark': 0,
'is_bridge_member': False,
- 'fwmark': 0x00,
'mtu': 1420,
'peer': [],
'peer_remove': [], # stores public keys of peers to remove
@@ -78,11 +78,12 @@ def get_config():
wg = deepcopy(default_config_data)
wg['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ # check if interface is member if a bridge
+ wg['is_bridge_member'] = is_member(conf, wg['intf'], 'bridge')
+
# 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']])
@@ -106,7 +107,7 @@ def get_config():
# local port to listen on
if conf.exists(['port']):
- wg['lport'] = conf.return_value(['port'])
+ wg['listen_port'] = conf.return_value(['port'])
# fwmark value
if conf.exists(['fwmark']):
@@ -189,38 +190,52 @@ def get_config():
def verify(wg):
- interface = wg['intf']
-
if wg['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wg["intf"]}" as it is a member '
+ f'of bridge "{wg["is_bridge_member"]}"!'))
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 wg['is_bridge_member'] and wg['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wg["intf"]}" '
+ f'as it is a member of bridge "{wg["is_bridge_member"]}"!'))
+
+ if wg['vrf']:
+ if wg['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wg["vrf"]}" does not exist')
+
+ if wg['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wg["intf"]}" cannot be member of VRF '
+ f'"{wg["vrf"]}" and bridge {wg["is_bridge_member"]} '
+ f'at the same time!'))
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}"!')
+ raise ConfigError(f'IP address required for interface "{wg["intf"]}"!')
if not wg['peer']:
- raise ConfigError(f'Peer required for interface "{interface}"!')
+ raise ConfigError(f'Peer required for interface "{wg["intf"]}"!')
# 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}"!')
+ 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}"!')
+ raise ConfigError(f'Peer public-key required for peer "{peer["name"]}"!')
+
+ if peer['address'] and not peer['port']:
+ raise ConfigError(f'Peer "{peer["name"]}" port must be defined if address is defined!')
+
+ if not peer['address'] and peer['port']:
+ raise ConfigError(f'Peer "{peer["name"]}" address must be defined if port is defined!')
def apply(wg):
@@ -246,8 +261,10 @@ def apply(wg):
# update interface description used e.g. within SNMP
w.set_alias(wg['description'])
- # assign/remove VRF
- w.set_vrf(wg['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wg['is_bridge_member']:
+ w.set_vrf(wg['vrf'])
# remove peers
for pub_key in wg['peer_remove']:
@@ -263,16 +280,18 @@ def apply(wg):
# peer allowed-ips
w.config['allowed-ips'] = peer['allowed-ips']
# local listen port
- if wg['lport']:
- w.config['port'] = wg['lport']
+ if wg['listen_port']:
+ w.config['port'] = wg['listen_port']
# fwmark
if c['fwmark']:
w.config['fwmark'] = wg['fwmark']
# endpoint
if peer['address'] and peer['port']:
- w.config['endpoint'] = '{}:{}'.format(
- peer['address'], peer['port'])
+ if is_ipv6(peer['address']):
+ w.config['endpoint'] = '[{}]:{}'.format(peer['address'], peer['port'])
+ else:
+ w.config['endpoint'] = '{}:{}'.format(peer['address'], peer['port'])
# persistent-keepalive
if peer['persistent_keepalive']:
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
index 42842f9bd..f13408fa2 100755
--- a/src/conf_mode/interfaces-wireless.py
+++ b/src/conf_mode/interfaces-wireless.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019 VyOS maintainers and contributors
+# 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
@@ -24,17 +24,16 @@ 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.configdict import list_diff, intf_to_dict, add_to_dict, interface_default_data
+from vyos.ifconfig import WiFiIf, Section
+from vyos.ifconfig_vlan import apply_all_vlans, verify_vlan_config
from vyos.template import render
from vyos.util import chown, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
- 'address': [],
- 'address_remove': [],
+ **interface_default_data,
'cap_ht' : False,
'cap_ht_40mhz_incapable' : False,
'cap_ht_powersave' : False,
@@ -69,30 +68,13 @@ default_config_data = {
'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',
@@ -107,9 +89,10 @@ default_config_data = {
'sec_wpa_radius' : [],
'ssid' : '',
'op_mode' : 'monitor',
- 'vif': [],
+ 'vif': {},
'vif_remove': [],
- 'vrf': ''
+ 'vif_s': {},
+ 'vif_s_remove': []
}
def get_conf_file(conf_type, intf):
@@ -124,21 +107,21 @@ def get_conf_file(conf_type, 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']
+ ifname = os.environ['VYOS_TAGNODE_VALUE']
+ conf = Config()
# check if wireless interface has been removed
- cfg_base = 'interfaces wireless ' + wifi['intf']
+ cfg_base = ['interfaces', 'wireless ', ifname]
if not conf.exists(cfg_base):
+ wifi = deepcopy(default_config_data)
+ wifi['intf'] = ifname
wifi['deleted'] = True
- # check if interface is member if a bridge
- wifi['is_bridge_member'] = is_bridge_member(conf, wifi['intf'])
+ # we need to know if we're a bridge member so we can refuse deletion
+ wifi['is_bridge_member'] = is_member(conf, wifi['intf'], 'bridge')
# we can not bail out early as wireless interface can not be removed
# Kernel will complain with: RTNETLINK answers: Operation not supported.
# Thus we need to remove individual settings
@@ -147,14 +130,8 @@ def get_config():
# 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'])
+ # get common interface settings
+ wifi, disabled = intf_to_dict(conf, default_config_data)
# 40MHz intolerance, use 20MHz only
if conf.exists('capabilities ht 40mhz-incapable'):
@@ -308,38 +285,10 @@ def get_config():
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
@@ -352,46 +301,10 @@ def get_config():
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')
@@ -404,10 +317,6 @@ def get_config():
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')
@@ -503,24 +412,6 @@ def get_config():
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'):
@@ -532,9 +423,9 @@ def get_config():
def verify(wifi):
if wifi['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wifi["intf"]}" as it is a '
+ f'member of bridge "{wifi["is_bridge_member"]}"!'))
return None
@@ -579,9 +470,23 @@ def verify(wifi):
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')
+ if ( wifi['is_bridge_member']
+ and ( wifi['address']
+ or wifi['ipv6_eui64_prefix']
+ or wifi['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{wifi["intf"]}" '
+ f'as it is a member of bridge "{wifi["is_bridge_member"]}"!'))
+
+ if wifi['vrf']:
+ if wifi['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wifi["vrf"]}" does not exist')
+
+ if wifi['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wifi["intf"]}" cannot be member of VRF '
+ f'"{wifi["vrf"]}" and bridge {wifi["is_bridge_member"]} '
+ f'at the same time!'))
# use common function to verify VLAN configuration
verify_vlan_config(wifi)
@@ -672,8 +577,10 @@ def apply(wifi):
# Finally create the new interface
w = WiFiIf(interface, **conf)
- # assign/remove VRF
- w.set_vrf(wifi['vrf'])
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not wifi['is_bridge_member']:
+ w.set_vrf(wifi['vrf'])
# update interface description used e.g. within SNMP
w.set_alias(wifi['description'])
@@ -693,9 +600,16 @@ def apply(wifi):
if wifi['dhcpv6_temporary']:
w.dhcp.v6.options['dhcpv6_temporary'] = True
+ if wifi['dhcpv6_pd']:
+ w.dhcp.v6.options['dhcpv6_pd'] = wifi['dhcpv6_pd']
+
# ignore link state changes
w.set_link_detect(wifi['disable_link_detect'])
+ # Delete old IPv6 EUI64 addresses before changing MAC
+ for addr in wifi['ipv6_eui64_prefix_remove']:
+ w.del_ipv6_eui64_address(addr)
+
# Change interface MAC address - re-set to real hardware address (hw-id)
# if custom mac is removed
if wifi['mac']:
@@ -703,6 +617,10 @@ def apply(wifi):
elif wifi['hw_id']:
w.set_mac(wifi['hw_id'])
+ # Add IPv6 EUI-based addresses
+ for addr in wifi['ipv6_eui64_prefix']:
+ w.add_ipv6_eui64_address(addr)
+
# configure ARP filter configuration
w.set_arp_filter(wifi['ip_disable_arp_filter'])
# configure ARP accept
@@ -711,10 +629,10 @@ def apply(wifi):
w.set_arp_announce(wifi['ip_enable_arp_announce'])
# configure ARP ignore
w.set_arp_ignore(wifi['ip_enable_arp_ignore'])
+ # IPv6 accept RA
+ w.set_ipv6_accept_ra(wifi['ipv6_accept_ra'])
# IPv6 address autoconfiguration
w.set_ipv6_autoconf(wifi['ipv6_autoconf'])
- # IPv6 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
@@ -728,24 +646,8 @@ def apply(wifi):
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)
+ # apply all vlans to interface
+ apply_all_vlans(w, wifi)
# Enable/Disable interface - interface is always placed in
# administrative down state in WiFiIf class
diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py
index 163778e22..975e21d9f 100755
--- a/src/conf_mode/interfaces-wirelessmodem.py
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -21,9 +21,10 @@ from copy import deepcopy
from netifaces import interfaces
from vyos.config import Config
+from vyos.ifconfig import BridgeIf, Section
from vyos.template import render
from vyos.util import chown, chmod_755, cmd, call
-from vyos.validate import is_bridge_member
+from vyos.validate import is_member
from vyos import ConfigError
default_config_data = {
@@ -64,11 +65,12 @@ def get_config():
wwan['logfile'] = f"/var/log/vyatta/ppp_{wwan['intf']}.log"
wwan['chat_script'] = f"/etc/ppp/peers/chat.{wwan['intf']}"
+ # check if interface is member if a bridge
+ wwan['is_bridge_member'] = is_member(conf, wwan['intf'], 'bridge')
+
# 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
@@ -119,9 +121,9 @@ def get_config():
def verify(wwan):
if wwan['deleted']:
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}"!')
+ raise ConfigError((
+ f'Cannot delete interface "{wwan["intf"]}" as it is a '
+ f'member of bridge "{wwan["is_bridge_member"]}"!'))
return None
@@ -133,9 +135,20 @@ def verify(wwan):
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')
+ if wwan['is_bridge_member'] and wwan['address']:
+ raise ConfigError((
+ f'Cannot assign address to interface "{wwan["intf"]}" '
+ f'as it is a member of bridge "{wwan["is_bridge_member"]}"!'))
+
+ if wwan['vrf']:
+ if wwan['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{wwan["vrf"]}" does not exist')
+
+ if wwan['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{wwan["intf"]}" cannot be member of VRF '
+ f'"{wwan["vrf"]}" and bridge {wwan["is_bridge_member"]} '
+ f'at the same time!'))
return None
@@ -152,12 +165,6 @@ def generate(wwan):
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')
@@ -172,17 +179,18 @@ def generate(wwan):
render(config_wwan, 'wwan/peer.tmpl', wwan)
# Create PPP chat script
render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
+
+ # generated script file must be executable
+
# Create script for ip-pre-up.d
- render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl', wwan)
+ render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl',
+ wwan, permission=0o755)
# Create script for ip-up.d
- render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', wwan)
+ render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl',
+ wwan, permission=0o755)
# 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)
+ render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl',
+ wwan, permission=0o755)
return None
@@ -198,6 +206,12 @@ def apply(wwan):
# make logfile owned by root / vyattacfg
chown(wwan['logfile'], 'root', 'vyattacfg')
+ # re-add ourselves to any bridge we might have fallen out of
+ # FIXME: wwan isn't under vyos.ifconfig so we can't call
+ # Interfaces.add_to_bridge() so STP settings won't get applied
+ if wwan['is_bridge_member'] in Section.interfaces('bridge'):
+ BridgeIf(wwan['is_bridge_member'], create=False).add_port(wwan['intf'])
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py
index 3398bcdf2..6282c2cc7 100755
--- a/src/conf_mode/ipsec-settings.py
+++ b/src/conf_mode/ipsec-settings.py
@@ -99,7 +99,7 @@ def get_config():
def write_ipsec_secrets(c):
if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret":
secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end)
- elif data.get("ipsec_l2tp_auth_mode") == "x509":
+ elif c.get("ipsec_l2tp_auth_mode") == "x509":
secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end)
old_umask = os.umask(0o077)
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
new file mode 100755
index 000000000..d491395ac
--- /dev/null
+++ b/src/conf_mode/nat.py
@@ -0,0 +1,268 @@
+#!/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 jmespath
+import json
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, cmd
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+
+default_config_data = {
+ 'deleted': False,
+ 'destination': [],
+ 'helper_functions': None,
+ 'pre_ct_helper': '',
+ 'pre_ct_conntrack': '',
+ 'out_ct_helper': '',
+ 'out_ct_conntrack': '',
+ 'source': []
+}
+
+iptables_nat_config = '/tmp/vyos-nat-rules.nft'
+
+def _check_kmod():
+ """ load required Kernel modules """
+ modules = ['nft_nat', 'nft_chain_nat_ipv4']
+ 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_handler(json, chain, target):
+ """ Get nftable rule handler number of given chain/target combination.
+ Handler is required when adding NAT/Conntrack helper targets """
+ for x in json:
+ if x['chain'] != chain:
+ continue
+ if x['target'] != target:
+ continue
+ return x['handle']
+
+ return None
+
+
+def verify_rule(rule, err_msg):
+ """ Common verify steps used for both source and destination NAT """
+ if rule['translation_port'] or rule['dest_port'] or rule['source_port']:
+ if rule['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ proto = rule['protocol']
+ raise ConfigError(f'{err_msg} ports can only be specified when protocol is "tcp", "udp" or "tcp_udp" (currently "{proto}")')
+
+ if '/' in rule['translation_address']:
+ raise ConfigError(f'{err_msg}\n' \
+ 'Cannot use ports with an IPv4net type translation address as it\n' \
+ 'statically maps a whole network of addresses onto another\n' \
+ 'network of addresses')
+
+ if not rule['translation_address']:
+ raise ConfigError(f'{err_msg} translation address not specified')
+
+
+def parse_source_destination(conf, source_dest):
+ """ Common wrapper to read in both NAT source and destination CLI """
+ tmp = []
+ base_level = ['nat', source_dest]
+ conf.set_level(base_level)
+ for number in conf.list_nodes(['rule']):
+ rule = {
+ 'description': '',
+ 'dest_address': '',
+ 'dest_port': '',
+ 'disabled': False,
+ 'exclude': False,
+ 'interface_in': '',
+ 'interface_out': '',
+ 'log': False,
+ 'protocol': 'all',
+ 'number': number,
+ 'source_address': '',
+ 'source_prefix': '',
+ 'source_port': '',
+ 'translation_address': '',
+ 'translation_prefix': '',
+ 'translation_port': ''
+ }
+ conf.set_level(base_level + ['rule', number])
+
+ if conf.exists(['description']):
+ rule['description'] = conf.return_value(['description'])
+
+ if conf.exists(['destination', 'address']):
+ rule['dest_address'] = conf.return_value(['destination', 'address'])
+
+ if conf.exists(['destination', 'port']):
+ rule['dest_port'] = conf.return_value(['destination', 'port'])
+
+ if conf.exists(['disable']):
+ rule['disabled'] = True
+
+ if conf.exists(['exclude']):
+ rule['exclude'] = True
+
+ if conf.exists(['inbound-interface']):
+ rule['interface_in'] = conf.return_value(['inbound-interface'])
+
+ if conf.exists(['outbound-interface']):
+ rule['interface_out'] = conf.return_value(['outbound-interface'])
+
+ if conf.exists(['log']):
+ rule['log'] = True
+
+ if conf.exists(['protocol']):
+ rule['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['source', 'address']):
+ rule['source_address'] = conf.return_value(['source', 'address'])
+
+ if conf.exists(['source', 'prefix']):
+ rule['source_prefix'] = conf.return_value(['source', 'prefix'])
+
+ if conf.exists(['source', 'port']):
+ rule['source_port'] = conf.return_value(['source', 'port'])
+
+ if conf.exists(['translation', 'address']):
+ rule['translation_address'] = conf.return_value(['translation', 'address'])
+
+ if conf.exists(['translation', 'prefix']):
+ rule['translation_prefix'] = conf.return_value(['translation', 'prefix'])
+
+ if conf.exists(['translation', 'port']):
+ rule['translation_port'] = conf.return_value(['translation', 'port'])
+
+ tmp.append(rule)
+
+ return tmp
+
+def get_config():
+ nat = deepcopy(default_config_data)
+ conf = Config()
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(['nat']):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+
+ nat['deleted'] = True
+
+ return nat
+
+ # check if NAT connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+
+ # set config level for parsing in NAT configuration
+ conf.set_level(['nat'])
+
+ # use a common wrapper function to read in the source / destination
+ # tree from the config - thus we do not need to replicate almost the
+ # same code :-)
+ for tgt in ['source', 'destination', 'nptv6']:
+ nat[tgt] = parse_source_destination(conf, tgt)
+
+ return nat
+
+def verify(nat):
+ if nat['deleted']:
+ # no need to verify the CLI as NAT is going to be deactivated
+ return None
+
+ if nat['helper_functions']:
+ if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']):
+ raise Exception('could not determine nftable ruleset handlers')
+
+ for rule in nat['source']:
+ interface = rule['interface_out']
+ err_msg = f"Source NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_out']:
+ raise ConfigError(f'{err_msg} outbound-interface not specified')
+
+ if rule['translation_address']:
+ addr = rule['translation_address']
+ if addr != 'masquerade' and not is_addr_assigned(addr):
+ print(f'Warning: IP address {addr} does not exist on the system!')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ for rule in nat['destination']:
+ interface = rule['interface_in']
+ err_msg = f"Destination NAT configuration error in rule {rule['number']}:"
+
+ if interface and interface not in interfaces():
+ print(f'NAT configuration warning: interface {interface} does not exist on this system')
+
+ if not rule['interface_in']:
+ raise ConfigError(f'{err_msg} inbound-interface not specified')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755)
+
+ return None
+
+def apply(nat):
+ cmd(f'{iptables_nat_config}')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ 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/protocols_bfd.py b/src/conf_mode/protocols_bfd.py
index ed8c3637b..d6577579e 100755
--- a/src/conf_mode/protocols_bfd.py
+++ b/src/conf_mode/protocols_bfd.py
@@ -190,7 +190,7 @@ def generate(bfd):
if bfd is None:
return None
- render(config_file, 'frr-bfd/bfd.frr.tmpl', bfd)
+ render(config_file, 'frr/bfd.frr.tmpl', bfd)
return None
def apply(bfd):
diff --git a/src/conf_mode/protocols_igmp.py b/src/conf_mode/protocols_igmp.py
index 9b338c5b9..821ccb0fc 100755
--- a/src/conf_mode/protocols_igmp.py
+++ b/src/conf_mode/protocols_igmp.py
@@ -87,7 +87,7 @@ def generate(igmp):
if igmp is None:
return None
- render(config_file, 'igmp/igmp.frr.tmpl', igmp)
+ render(config_file, 'frr/igmp.frr.tmpl', igmp)
return None
def apply(igmp):
diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py
index 0a241277d..9b946b43a 100755
--- a/src/conf_mode/protocols_mpls.py
+++ b/src/conf_mode/protocols_mpls.py
@@ -127,7 +127,7 @@ def generate(mpls):
if mpls is None:
return None
- render(config_file, 'mpls/ldpd.frr.tmpl', mpls)
+ render(config_file, 'frr/ldpd.frr.tmpl', mpls)
return None
def apply(mpls):
diff --git a/src/conf_mode/protocols_pim.py b/src/conf_mode/protocols_pim.py
index f12de4a72..15c4a2b0f 100755
--- a/src/conf_mode/protocols_pim.py
+++ b/src/conf_mode/protocols_pim.py
@@ -114,7 +114,7 @@ def generate(pim):
if pim is None:
return None
- render(config_file, 'pim/pimd.frr.tmpl', pim)
+ render(config_file, 'frr/pimd.frr.tmpl', pim)
return None
def apply(pim):
diff --git a/src/conf_mode/protocols_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
index 411a130ec..ba6324393 100755
--- a/src/conf_mode/protocols_static_multicast.py
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -91,7 +91,7 @@ def generate(mroute):
if mroute is None:
return None
- render(config_file, 'frr-mcast/static_mcast.frr.tmpl', mroute)
+ render(config_file, 'frr/static_mcast.frr.tmpl', mroute)
return None
def apply(mroute):
diff --git a/src/conf_mode/salt-minion.py b/src/conf_mode/salt-minion.py
index 236480854..8bc35bb45 100755
--- a/src/conf_mode/salt-minion.py
+++ b/src/conf_mode/salt-minion.py
@@ -17,117 +17,102 @@
import os
from copy import deepcopy
-from pwd import getpwnam
from socket import gethostname
from sys import exit
from urllib3 import PoolManager
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call
from vyos.template import render
-
+from vyos.util import call, chown
+from vyos import ConfigError
config_file = r'/etc/salt/minion'
+master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
default_config_data = {
- 'hash_type': 'sha256',
- 'log_file': '/var/log/salt/minion',
+ 'hash': 'sha256',
'log_level': 'warning',
'master' : 'salt',
'user': 'minion',
+ 'group': 'vyattacfg',
'salt_id': gethostname(),
'mine_interval': '60',
- 'verify_master_pubkey_sign': 'false'
+ 'verify_master_pubkey_sign': 'false',
+ 'master_key': ''
}
def get_config():
salt = deepcopy(default_config_data)
conf = Config()
- if not conf.exists('service salt-minion'):
+ base = ['service', 'salt-minion']
+
+ if not conf.exists(base):
return None
else:
- conf.set_level('service salt-minion')
-
- if conf.exists('hash_type'):
- salt['hash_type'] = conf.return_value('hash_type')
-
- if conf.exists('log_file'):
- salt['log_file'] = conf.return_value('log_file')
+ conf.set_level(base)
- if conf.exists('log_level'):
- salt['log_level'] = conf.return_value('log_level')
+ if conf.exists(['hash']):
+ salt['hash'] = conf.return_value(['hash'])
- if conf.exists('master'):
- master = conf.return_values('master')
- salt['master'] = master
+ if conf.exists(['master']):
+ salt['master'] = conf.return_values(['master'])
- if conf.exists('id'):
- salt['salt_id'] = conf.return_value('id')
+ if conf.exists(['id']):
+ salt['salt_id'] = conf.return_value(['id'])
- if conf.exists('user'):
- salt['user'] = conf.return_value('user')
+ if conf.exists(['user']):
+ salt['user'] = conf.return_value(['user'])
- if conf.exists('mine_interval'):
- salt['mine_interval'] = conf.return_value('mine_interval')
+ if conf.exists(['interval']):
+ salt['interval'] = conf.return_value(['interval'])
- salt['master-key'] = None
- if conf.exists('master-key'):
- salt['master-key'] = conf.return_value('master-key')
+ if conf.exists(['master-key']):
+ salt['master_key'] = conf.return_value(['master-key'])
salt['verify_master_pubkey_sign'] = 'true'
return salt
-def generate(salt):
- paths = ['/etc/salt/','/var/run/salt','/opt/vyatta/etc/config/salt/']
- directory = '/opt/vyatta/etc/config/salt/pki/minion'
- uid = getpwnam(salt['user']).pw_uid
- http = PoolManager()
+def verify(salt):
+ return None
- if salt is None:
+def generate(salt):
+ if not salt:
return None
- if not os.path.exists(directory):
- os.makedirs(directory)
-
- render(config_file, 'salt-minion/minion.tmpl', salt)
-
- path = "/etc/salt/"
- for path in paths:
- for root, dirs, files in os.walk(path):
- for usgr in dirs:
- os.chown(os.path.join(root, usgr), uid, 100)
- for usgr in files:
- os.chown(os.path.join(root, usgr), uid, 100)
+ render(config_file, 'salt-minion/minion.tmpl', salt,
+ user=salt['user'], group=salt['group'])
- if not os.path.exists('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'):
- if not salt['master-key'] is None:
- r = http.request('GET', salt['master-key'], preload_content=False)
+ if not os.path.exists(master_keyfile):
+ if salt['master_key']:
+ req = PoolManager().request('GET', salt['master_key'], preload_content=False)
- with open('/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub', 'wb') as out:
+ with open(master_keyfile, 'wb') as f:
while True:
- data = r.read(1024)
+ data = req.read(1024)
if not data:
break
- out.write(data)
+ f.write(data)
- r.release_conn()
+ req.release_conn()
+ chown(master_keyfile, salt['user'], salt['group'])
return None
def apply(salt):
- if salt is not None:
- call("sudo systemctl restart salt-minion")
+ if not salt:
+ # Salt removed from running config
+ call('systemctl stop salt-minion.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
else:
- # Salt access is removed in the commit
- call("sudo systemctl stop salt-minion")
- os.unlink(config_file)
+ call('systemctl restart salt-minion.service')
return None
if __name__ == '__main__':
try:
c = get_config()
+ verify(c)
generate(c)
apply(c)
except ConfigError as e:
diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py
index 17fa2c3f0..84443ade3 100755
--- a/src/conf_mode/service_ipoe-server.py
+++ b/src/conf_mode/service_ipoe-server.py
@@ -112,28 +112,30 @@ def get_config():
'name': interface,
'mac': []
}
- for client in conf.list_nodes(base_path + ['authentication', 'interface', interface, 'mac-address']):
- mac = {
+ for mac in conf.list_nodes(['authentication', 'interface', interface, 'mac-address']):
+ client = {
'address': mac,
'rate_download': '',
'rate_upload': '',
'vlan_id': ''
}
- conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', client])
+ conf.set_level(base_path + ['authentication', 'interface', interface, 'mac-address', mac])
if conf.exists(['rate-limit', 'download']):
- mac['rate_download'] = conf.return_value(['rate-limit', 'download'])
+ client['rate_download'] = conf.return_value(['rate-limit', 'download'])
if conf.exists(['rate-limit', 'upload']):
- mac['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
+ client['rate_upload'] = conf.return_value(['rate-limit', 'upload'])
if conf.exists(['vlan-id']):
- mac['vlan'] = conf.return_value(['vlan-id'])
+ client['vlan'] = conf.return_value(['vlan-id'])
- tmp['mac'].append(mac)
+ tmp['mac'].append(client)
ipoe['auth_interfaces'].append(tmp)
+ conf.set_level(base_path)
+
#
# authentication mode radius servers and settings
if conf.exists(['authentication', 'mode', 'radius']):
@@ -265,10 +267,6 @@ def generate(ipoe):
if not ipoe:
return None
- dirname = os.path.dirname(ipoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True)
if ipoe['auth_mode'] == 'local':
diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
index 95cb066d8..e05b0ab2a 100755
--- a/src/conf_mode/service_pppoe-server.py
+++ b/src/conf_mode/service_pppoe-server.py
@@ -23,7 +23,7 @@ from sys import exit
from vyos.config import Config
from vyos.template import render
-from vyos.util import call, get_half_cpus()
+from vyos.util import call, get_half_cpus
from vyos.validate import is_ipv4
from vyos import ConfigError
@@ -32,6 +32,7 @@ pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets'
default_config_data = {
'auth_mode': 'local',
+ 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'],
'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template
'client_ip_pool': '',
'client_ip_subnets': [],
@@ -216,6 +217,19 @@ def get_config():
pppoe['local_users'].append(user)
conf.set_level(base_path)
+
+ if conf.exists(['authentication', 'protocols']):
+ auth_mods = {
+ 'mschap-v2': 'auth_mschap_v2',
+ 'mschap': 'auth_mschap_v1',
+ 'chap': 'auth_chap_md5',
+ 'pap': 'auth_pap'
+ }
+
+ pppoe['auth_proto'] = []
+ for proto in conf.return_values(['authentication', 'protocols']):
+ pppoe['auth_proto'].append(auth_mods[proto])
+
#
# authentication mode radius servers and settings
if conf.exists(['authentication', 'mode', 'radius']):
@@ -301,7 +315,7 @@ def get_config():
pppoe['mtu'] = conf.return_value(['mtu'])
if conf.exists(['session-control']):
- pppoe['session_control'] = conf.return_value(['session-control'])
+ pppoe['sesscrtl'] = conf.return_value(['session-control'])
# ppp_options
if conf.exists(['ppp-options']):
@@ -415,10 +429,6 @@ def generate(pppoe):
if not pppoe:
return None
- dirname = os.path.dirname(pppoe_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
if pppoe['local_users']:
diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py
index 91e2b369f..09c5422eb 100755
--- a/src/conf_mode/system-login.py
+++ b/src/conf_mode/system-login.py
@@ -17,6 +17,7 @@
import os
from crypt import crypt, METHOD_SHA512
+from netifaces import interfaces
from psutil import users
from pwd import getpwall, getpwnam
from stat import S_IRUSR, S_IWUSR, S_IRWXU, S_IRGRP, S_IXGRP
@@ -39,6 +40,7 @@ default_config_data = {
'del_users': [],
'radius_server': [],
'radius_source_address': '',
+ 'radius_vrf': ''
}
def get_local_users():
@@ -127,6 +129,10 @@ def get_config():
if conf.exists(['source-address']):
login['radius_source_address'] = conf.return_value(['source-address'])
+ # retrieve VRF instance
+ if conf.exists(['vrf']):
+ login['radius_vrf'] = conf.return_value(['vrf'])
+
# Read in all RADIUS servers and store to list
for server in conf.list_nodes(['server']):
server_cfg = {
@@ -193,6 +199,9 @@ def verify(login):
if fail:
raise ConfigError('At least one RADIUS server must be active.')
+ vrf_name = login['radius_vrf']
+ if vrf_name and vrf_name not in interfaces():
+ raise ConfigError(f'VRF "{vrf_name}" does not exist')
return None
@@ -217,7 +226,7 @@ def generate(login):
# env=env)
if len(login['radius_server']) > 0:
- render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login)
+ render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', login, trim_blocks=True)
uid = getpwnam('root').pw_uid
gid = getpwnam('root').pw_gid
diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py
index a4ef99d45..f312f2a17 100755
--- a/src/conf_mode/vpn_l2tp.py
+++ b/src/conf_mode/vpn_l2tp.py
@@ -340,10 +340,6 @@ def generate(l2tp):
if not l2tp:
return None
- dirname = os.path.dirname(l2tp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True)
if l2tp['auth_mode'] == 'local':
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index 046fc8f9c..085c9c2c6 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -247,10 +247,6 @@ def generate(pptp):
if not pptp:
return None
- dirname = os.path.dirname(pptp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True)
if pptp['local_users']:
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index e6ce94709..7c3e3f515 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -22,10 +22,10 @@ from copy import deepcopy
from stat import S_IRUSR, S_IWUSR, S_IRGRP
from vyos.config import Config
-from vyos import ConfigError
-from vyos.util import call, run, get_half_cpus
from vyos.template import render
-
+from vyos.util import call, run, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
sstp_conf = '/run/accel-pppd/sstp.conf'
sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
@@ -35,7 +35,12 @@ default_config_data = {
'auth_mode' : 'local',
'auth_proto' : ['auth_mschap_v2'],
'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool' : [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
'client_gateway': '',
+ 'dnsv4' : [],
+ 'dnsv6' : [],
'radius_server' : [],
'radius_acct_tmo' : '3',
'radius_max_try' : '3',
@@ -49,8 +54,6 @@ default_config_data = {
'ssl_ca' : '',
'ssl_cert' : '',
'ssl_key' : '',
- 'client_ip_pool' : [],
- 'dnsv4' : [],
'mtu' : '',
'ppp_mppe' : 'prefer',
'ppp_echo_failure' : '',
@@ -210,7 +213,7 @@ def get_config():
#
- # read in client ip pool settings
+ # read in client IPv4 pool
conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
if conf.exists(['subnet']):
sstp['client_ip_pool'] = conf.return_values(['subnet'])
@@ -219,10 +222,41 @@ def get_config():
sstp['client_gateway'] = conf.return_value(['gateway-address'])
#
+ # read in client IPv6 pool
+ conf.set_level(base_path + ['network-settings', 'client-ipv6-pool'])
+ if conf.exists(['prefix']):
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['prefix', prefix, 'mask'])
+
+ sstp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['delegate']):
+ for prefix in conf.list_nodes(['delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix'])
+
+ sstp['client_ipv6_delegate_prefix'].append(tmp)
+
+ #
# read in network settings
conf.set_level(base_path + ['network-settings'])
if conf.exists(['name-server']):
- sstp['dnsv4'] = conf.return_values(['name-server'])
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ sstp['dnsv4'].append(name_server)
+ else:
+ sstp['dnsv6'].append(name_server)
if conf.exists(['mtu']):
sstp['mtu'] = conf.return_value(['mtu'])
@@ -275,6 +309,14 @@ def verify(sstp):
if len(sstp['dnsv4']) > 2:
raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+ # check ipv6
+ if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in sstp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
raise ConfigError('One or more SSL certificates missing')
@@ -303,10 +345,6 @@ def generate(sstp):
if not sstp:
return None
- dirname = os.path.dirname(sstp_conf)
- if not os.path.exists(dirname):
- os.mkdir(dirname)
-
# accel-cmd reload doesn't work so any change results in a restart of the daemon
render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp, trim_blocks=True)