diff options
Diffstat (limited to 'src/conf_mode')
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) |