diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 39b80ce08..43cd7220a 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1,1233 +1,1233 @@ # Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os import re import json import jmespath from copy import deepcopy from glob import glob from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from netifaces import ifaddresses # this is not the same as socket.AF_INET/INET6 from netifaces import AF_INET from netifaces import AF_INET6 from vyos import ConfigError from vyos.configdict import list_diff from vyos.configdict import dict_merge from vyos.template import render from vyos.util import mac2eui64 from vyos.util import dict_search from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.validate import is_intf_addr_assigned from vyos.validate import assert_boolean from vyos.validate import assert_list from vyos.validate import assert_mac from vyos.validate import assert_mtu from vyos.validate import assert_positive from vyos.validate import assert_range from vyos.ifconfig.control import Control from vyos.ifconfig.vrrp import VRRP from vyos.ifconfig.operational import Operational from vyos.ifconfig import Section class Interface(Control): # This is the class which will be used to create # self.operational, it allows subclasses, such as # WireGuard to modify their display behaviour OperationalClass = Operational options = ['debug', 'create'] required = [] default = { 'type': '', 'debug': True, 'create': True, } definition = { 'section': '', 'prefixes': [], 'vlan': False, 'bondable': False, 'broadcast': False, 'bridgeable': False, 'eternal': '', } _command_get = { 'admin_state': { 'shellcmd': 'ip -json link show dev {ifname}', 'format': lambda j: 'up' if 'UP' in jmespath.search('[*].flags | [0]', json.loads(j)) else 'down', }, 'min_mtu': { 'shellcmd': 'ip -json -detail link list dev {ifname}', 'format': lambda j: jmespath.search('[*].min_mtu | [0]', json.loads(j)), }, 'max_mtu': { 'shellcmd': 'ip -json -detail link list dev {ifname}', 'format': lambda j: jmespath.search('[*].max_mtu | [0]', json.loads(j)), }, } _command_set = { 'admin_state': { 'validate': lambda v: assert_list(v, ['up', 'down']), 'shellcmd': 'ip link set dev {ifname} {value}', }, 'mac': { 'validate': assert_mac, 'shellcmd': 'ip link set dev {ifname} address {value}', }, 'vrf': { 'convert': lambda v: f'master {v}' if v else 'nomaster', 'shellcmd': 'ip link set dev {ifname} {value}', }, } _sysfs_get = { 'alias': { 'location': '/sys/class/net/{ifname}/ifalias', }, 'mac': { 'location': '/sys/class/net/{ifname}/address', }, 'mtu': { 'location': '/sys/class/net/{ifname}/mtu', }, 'oper_state':{ 'location': '/sys/class/net/{ifname}/operstate', }, } _sysfs_set = { 'alias': { 'convert': lambda name: name if name else '\0', 'location': '/sys/class/net/{ifname}/ifalias', }, 'mtu': { 'validate': assert_mtu, 'location': '/sys/class/net/{ifname}/mtu', }, 'arp_cache_tmo': { 'convert': lambda tmo: (int(tmo) * 1000), 'location': '/proc/sys/net/ipv4/neigh/{ifname}/base_reachable_time_ms', }, 'arp_filter': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_filter', }, 'arp_accept': { 'validate': lambda arp: assert_range(arp,0,2), 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_accept', }, 'arp_announce': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_announce', }, 'arp_ignore': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/arp_ignore', }, 'ipv4_forwarding': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/forwarding', }, 'ipv6_accept_ra': { 'validate': lambda ara: assert_range(ara,0,3), 'location': '/proc/sys/net/ipv6/conf/{ifname}/accept_ra', }, 'ipv6_autoconf': { 'validate': lambda aco: assert_range(aco,0,2), 'location': '/proc/sys/net/ipv6/conf/{ifname}/autoconf', }, 'ipv6_forwarding': { 'validate': lambda fwd: assert_range(fwd,0,2), 'location': '/proc/sys/net/ipv6/conf/{ifname}/forwarding', }, 'ipv6_dad_transmits': { 'validate': assert_positive, 'location': '/proc/sys/net/ipv6/conf/{ifname}/dad_transmits', }, 'path_cost': { # XXX: we should set a maximum 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/brport/path_cost', 'errormsg': '{ifname} is not a bridge port member' }, 'path_priority': { # XXX: we should set a maximum 'validate': assert_positive, 'location': '/sys/class/net/{ifname}/brport/priority', 'errormsg': '{ifname} is not a bridge port member' }, 'proxy_arp': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp', }, 'proxy_arp_pvlan': { 'validate': assert_boolean, 'location': '/proc/sys/net/ipv4/conf/{ifname}/proxy_arp_pvlan', }, # link_detect vs link_filter name weirdness 'link_detect': { 'validate': lambda link: assert_range(link,0,3), 'location': '/proc/sys/net/ipv4/conf/{ifname}/link_filter', }, } @classmethod def exists(cls, ifname): return os.path.exists(f'/sys/class/net/{ifname}') @classmethod def get_config(cls): """ Some but not all interfaces require a configuration when they are added using iproute2. This method will provide the configuration dictionary used by this class. """ return deepcopy(cls.default) def __init__(self, ifname, **kargs): """ This is the base interface class which supports basic IP/MAC address operations as well as DHCP(v6). Other interface which represent e.g. and ethernet bridge are implemented as derived classes adding all additional functionality. For creation you will need to provide the interface type, otherwise the existing interface is used DEBUG: This class has embedded debugging (print) which can be enabled by creating the following file: vyos@vyos# touch /tmp/vyos.ifconfig.debug Example: >>> from vyos.ifconfig import Interface >>> i = Interface('eth0') """ self.config = deepcopy(self.default) for k in self.options: if k in kargs: self.config[k] = kargs[k] # make sure the ifname is the first argument and not from the dict self.config['ifname'] = ifname self._admin_state_down_cnt = 0 # we must have updated config before initialising the Interface super().__init__(**kargs) self.ifname = ifname if not self.exists(ifname): # Any instance of Interface, such as Interface('eth0') # can be used safely to access the generic function in this class # as 'type' is unset, the class can not be created if not self.config['type']: raise Exception(f'interface "{ifname}" not found') # Should an Instance of a child class (EthernetIf, DummyIf, ..) # be required, then create should be set to False to not accidentally create it. # In case a subclass does not define it, we use get to set the default to True if self.config.get('create',True): for k in self.required: if k not in kargs: name = self.default['type'] raise ConfigError(f'missing required option {k} for {name} {ifname} creation') self._create() # If we can not connect to the interface then let the caller know # as the class could not be correctly initialised else: raise Exception('interface "{}" not found'.format(self.config['ifname'])) # temporary list of assigned IP addresses self._addr = [] self.operational = self.OperationalClass(ifname) self.vrrp = VRRP(ifname) def _create(self): cmd = 'ip link add dev {ifname} type {type}'.format(**self.config) self._cmd(cmd) def remove(self): """ Remove interface from operating system. Removing the interface deconfigures all assigned IP addresses and clear possible DHCP(v6) client processes. Example: >>> from vyos.ifconfig import Interface >>> i = Interface('eth0') >>> i.remove() """ # remove all assigned IP addresses from interface - this is a bit redundant # as the kernel will remove all addresses on interface deletion, but we # can not delete ALL interfaces, see below self.flush_addrs() # --------------------------------------------------------------------- # Any class can define an eternal regex in its definition # interface matching the regex will not be deleted eternal = self.definition['eternal'] if not eternal: self._delete() elif not re.match(eternal, self.ifname): self._delete() def _delete(self): # NOTE (Improvement): # after interface removal no other commands should be allowed # to be called and instead should raise an Exception: cmd = 'ip link del dev {ifname}'.format(**self.config) return self._cmd(cmd) def get_min_mtu(self): """ Get hardware minimum supported MTU Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_min_mtu() '60' """ return int(self.get_interface('min_mtu')) def get_max_mtu(self): """ Get hardware maximum supported MTU Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_max_mtu() '9000' """ return int(self.get_interface('max_mtu')) def get_mtu(self): """ Get/set interface mtu in bytes. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_mtu() '1500' """ return int(self.get_interface('mtu')) def set_mtu(self, mtu): """ Get/set interface mtu in bytes. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_mtu(1400) >>> Interface('eth0').get_mtu() '1400' """ return self.set_interface('mtu', mtu) def get_mac(self): """ Get current interface MAC (Media Access Contrl) address used. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_mac() '00:50:ab:cd:ef:00' """ return self.get_interface('mac') def set_mac(self, mac): """ Set interface MAC (Media Access Contrl) address to given value. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_mac('00:50:ab:cd:ef:01') """ # If MAC is unchanged, bail out early if mac == self.get_mac(): return None # MAC address can only be changed if interface is in 'down' state prev_state = self.get_admin_state() if prev_state == 'up': self.set_admin_state('down') self.set_interface('mac', mac) # Turn an interface to the 'up' state if it was changed to 'down' by this fucntion if prev_state == 'up': self.set_admin_state('up') def set_vrf(self, vrf=''): """ Add/Remove interface from given VRF instance. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_vrf('foo') >>> Interface('eth0').set_vrf() """ self.set_interface('vrf', vrf) def set_arp_cache_tmo(self, tmo): """ Set ARP cache timeout value in seconds. Internal Kernel representation is in milliseconds. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_arp_cache_tmo(40) """ return self.set_interface('arp_cache_tmo', tmo) def set_arp_filter(self, arp_filter): """ Filter ARP requests 1 - Allows you to have multiple network interfaces on the same subnet, and have the ARPs for each interface be answered based on whether or not the kernel would route a packet from the ARP'd IP out that interface (therefore you must use source based routing for this to work). In other words it allows control of which cards (usually 1) will respond to an arp request. 0 - (default) The kernel can respond to arp requests with addresses from other interfaces. This may seem wrong but it usually makes sense, because it increases the chance of successful communication. IP addresses are owned by the complete host on Linux, not by particular interfaces. Only for more complex setups like load- balancing, does this behaviour cause problems. """ return self.set_interface('arp_filter', arp_filter) def set_arp_accept(self, arp_accept): """ Define behavior for gratuitous ARP frames who's IP is not already present in the ARP table: 0 - don't create new entries in the ARP table 1 - create new entries in the ARP table Both replies and requests type gratuitous arp will trigger the ARP table to be updated, if this setting is on. If the ARP table already contains the IP address of the gratuitous arp frame, the arp table will be updated regardless if this setting is on or off. """ return self.set_interface('arp_accept', arp_accept) def set_arp_announce(self, arp_announce): """ Define different restriction levels for announcing the local source IP address from IP packets in ARP requests sent on interface: 0 - (default) Use any local address, configured on any interface 1 - Try to avoid local addresses that are not in the target's subnet for this interface. This mode is useful when target hosts reachable via this interface require the source IP address in ARP requests to be part of their logical network configured on the receiving interface. When we generate the request we will check all our subnets that include the target IP and will preserve the source address if it is from such subnet. Increasing the restriction level gives more chance for receiving answer from the resolved target while decreasing the level announces more valid sender's information. """ return self.set_interface('arp_announce', arp_announce) def set_arp_ignore(self, arp_ignore): """ Define different modes for sending replies in response to received ARP requests that resolve local target IP addresses: 0 - (default): reply for any local target IP address, configured on any interface 1 - reply only if the target IP address is local address configured on the incoming interface """ return self.set_interface('arp_ignore', arp_ignore) def set_ipv4_forwarding(self, forwarding): """ Configure IPv4 forwarding. """ return self.set_interface('ipv4_forwarding', forwarding) def set_ipv6_accept_ra(self, accept_ra): """ Accept Router Advertisements; autoconfigure using them. It also determines whether or not to transmit Router Solicitations. If and only if the functional setting is to accept Router Advertisements, Router Solicitations will be transmitted. 0 - Do not accept Router Advertisements. 1 - (default) Accept Router Advertisements if forwarding is disabled. 2 - Overrule forwarding behaviour. Accept Router Advertisements even if forwarding is enabled. """ return self.set_interface('ipv6_accept_ra', accept_ra) def set_ipv6_autoconf(self, autoconf): """ Autoconfigure addresses using Prefix Information in Router Advertisements. """ return self.set_interface('ipv6_autoconf', autoconf) def add_ipv6_eui64_address(self, prefix): """ Extended Unique Identifier (EUI), as per RFC2373, allows a host to assign itself a unique IPv6 address based on a given IPv6 prefix. Calculate the EUI64 from the interface's MAC, then assign it with the given prefix to the interface. """ # T2863: only add a link-local IPv6 address if the interface returns # a MAC address. This is not the case on e.g. WireGuard interfaces. mac = self.get_mac() if mac: eui64 = mac2eui64(mac, prefix) prefixlen = prefix.split('/')[1] self.add_addr(f'{eui64}/{prefixlen}') def del_ipv6_eui64_address(self, prefix): """ Delete the address based on the interface's MAC-based EUI64 combined with the prefix address. """ eui64 = mac2eui64(self.get_mac(), prefix) prefixlen = prefix.split('/')[1] self.del_addr(f'{eui64}/{prefixlen}') def set_ipv6_forwarding(self, forwarding): """ Configure IPv6 interface-specific Host/Router behaviour. False: By default, Host behaviour is assumed. This means: 1. IsRouter flag is not set in Neighbour Advertisements. 2. If accept_ra is TRUE (default), transmit Router Solicitations. 3. If accept_ra is TRUE (default), accept Router Advertisements (and do autoconfiguration). 4. If accept_redirects is TRUE (default), accept Redirects. True: If local forwarding is enabled, Router behaviour is assumed. This means exactly the reverse from the above: 1. IsRouter flag is set in Neighbour Advertisements. 2. Router Solicitations are not sent unless accept_ra is 2. 3. Router Advertisements are ignored unless accept_ra is 2. 4. Redirects are ignored. """ return self.set_interface('ipv6_forwarding', forwarding) def set_ipv6_dad_messages(self, dad): """ The amount of Duplicate Address Detection probes to send. Default: 1 """ return self.set_interface('ipv6_dad_transmits', dad) def set_link_detect(self, link_filter): """ Configure kernel response in packets received on interfaces that are 'down' 0 - Allow packets to be received for the address on this interface even if interface is disabled or no carrier. 1 - Ignore packets received if interface associated with the incoming address is down. 2 - Ignore packets received if interface associated with the incoming address is down or has no carrier. Default value is 0. Note that some distributions enable it in startup scripts. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_link_detect(1) """ return self.set_interface('link_detect', link_filter) def get_alias(self): """ Get interface alias name used by e.g. SNMP Example: >>> Interface('eth0').get_alias() 'interface description as set by user' """ return self.get_interface('alias') def set_alias(self, ifalias=''): """ Set interface alias name used by e.g. SNMP Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_alias('VyOS upstream interface') to clear alias e.g. delete it use: >>> Interface('eth0').set_ifalias('') """ self.set_interface('alias', ifalias) def get_admin_state(self): """ Get interface administrative state. Function will return 'up' or 'down' Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_admin_state() 'up' """ return self.get_interface('admin_state') def set_admin_state(self, state): """ Set interface administrative state to be 'up' or 'down' Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_admin_state('down') >>> Interface('eth0').get_admin_state() 'down' """ if state == 'up': self._admin_state_down_cnt -= 1 if self._admin_state_down_cnt < 1: return self.set_interface('admin_state', state) else: self._admin_state_down_cnt += 1 return self.set_interface('admin_state', state) def set_path_cost(self, cost): """ Set interface path cost, only relevant for STP enabled interfaces Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_path_cost(4) """ self.set_interface('path_cost', cost) def set_path_priority(self, priority): """ Set interface path priority, only relevant for STP enabled interfaces Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_path_priority(4) """ self.set_interface('path_priority', priority) def set_proxy_arp(self, enable): """ Set per interface proxy ARP configuration Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_proxy_arp(1) """ self.set_interface('proxy_arp', enable) def set_proxy_arp_pvlan(self, enable): """ Private VLAN proxy arp. Basically allow proxy arp replies back to the same interface (from which the ARP request/solicitation was received). This is done to support (ethernet) switch features, like RFC 3069, where the individual ports are NOT allowed to communicate with each other, but they are allowed to talk to the upstream router. As described in RFC 3069, it is possible to allow these hosts to communicate through the upstream router by proxy_arp'ing. Don't need to be used together with proxy_arp. This technology is known by different names: In RFC 3069 it is called VLAN Aggregation. Cisco and Allied Telesyn call it Private VLAN. Hewlett-Packard call it Source-Port filtering or port-isolation. Ericsson call it MAC-Forced Forwarding (RFC Draft). Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').set_proxy_arp_pvlan(1) """ self.set_interface('proxy_arp_pvlan', enable) def get_addr(self): """ Retrieve assigned IPv4 and IPv6 addresses from given interface. This is done using the netifaces and ipaddress python modules. Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0').get_addrs() ['172.16.33.30/24', 'fe80::20c:29ff:fe11:a174/64'] """ ipv4 = [] ipv6 = [] if AF_INET in ifaddresses(self.config['ifname']).keys(): for v4_addr in ifaddresses(self.config['ifname'])[AF_INET]: # we need to manually assemble a list of IPv4 address/prefix prefix = '/' + \ str(IPv4Network('0.0.0.0/' + v4_addr['netmask']).prefixlen) ipv4.append(v4_addr['addr'] + prefix) if AF_INET6 in ifaddresses(self.config['ifname']).keys(): for v6_addr in ifaddresses(self.config['ifname'])[AF_INET6]: # Note that currently expanded netmasks are not supported. That means # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. # see https://docs.python.org/3/library/ipaddress.html bits = bin( int(v6_addr['netmask'].replace(':', ''), 16)).count('1') prefix = '/' + str(bits) # we alsoneed to remove the interface suffix on link local # addresses v6_addr['addr'] = v6_addr['addr'].split('%')[0] ipv6.append(v6_addr['addr'] + prefix) return ipv4 + ipv6 def add_addr(self, addr): """ Add IP(v6) address to interface. Address is only added if it is not already assigned to that interface. Address format must be validated and compressed/normalized before calling this function. addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! IPv4: add IPv4 address to interface IPv6: add IPv6 address to interface dhcp: start dhclient (IPv4) on interface dhcpv6: start WIDE DHCPv6 (IPv6) on interface Returns False if address is already assigned and wasn't re-added. Example: >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') >>> j.add_addr('192.0.2.1/24') >>> j.add_addr('2001:db8::ffff/64') >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] """ # XXX: normalize/compress with ipaddress if calling functions don't? # is subnet mask always passed, and in the same way? # do not add same address twice if addr in self._addr: return False addr_is_v4 = is_ipv4(addr) # we can't have both DHCP and static IPv4 addresses assigned for a in self._addr: if ( ( addr == 'dhcp' and a != 'dhcpv6' and is_ipv4(a) ) or ( a == 'dhcp' and addr != 'dhcpv6' and addr_is_v4 ) ): raise ConfigError(( "Can't configure both static IPv4 and DHCP address " "on the same interface")) # add to interface if addr == 'dhcp': self.set_dhcp(True) elif addr == 'dhcpv6': self.set_dhcpv6(True) elif not is_intf_addr_assigned(self.ifname, addr): self._cmd(f'ip addr add "{addr}" ' f'{"brd + " if addr_is_v4 else ""}dev "{self.ifname}"') else: return False # add to cache self._addr.append(addr) return True def del_addr(self, addr): """ Delete IP(v6) address from interface. Address is only deleted if it is assigned to that interface. Address format must be exactly the same as was used when adding the address. addr: can be an IPv4 address, IPv6 address, dhcp or dhcpv6! IPv4: delete IPv4 address from interface IPv6: delete IPv6 address from interface dhcp: stop dhclient (IPv4) on interface dhcpv6: stop dhclient (IPv6) on interface Returns False if address isn't already assigned and wasn't deleted. Example: >>> from vyos.ifconfig import Interface >>> j = Interface('eth0') >>> j.add_addr('2001:db8::ffff/64') >>> j.add_addr('192.0.2.1/24') >>> j.get_addr() ['192.0.2.1/24', '2001:db8::ffff/64'] >>> j.del_addr('192.0.2.1/24') >>> j.get_addr() ['2001:db8::ffff/64'] """ # remove from interface if addr == 'dhcp': self.set_dhcp(False) elif addr == 'dhcpv6': self.set_dhcpv6(False) elif is_intf_addr_assigned(self.ifname, addr): self._cmd(f'ip addr del "{addr}" dev "{self.ifname}"') else: return False # remove from cache if addr in self._addr: self._addr.remove(addr) return True def flush_addrs(self): """ Flush all addresses from an interface, including DHCP. Will raise an exception on error. """ # stop DHCP(v6) if running self.set_dhcp(False) self.set_dhcpv6(False) # flush all addresses self._cmd(f'ip addr flush dev "{self.ifname}"') def add_to_bridge(self, bridge_dict): """ Adds the interface to the bridge with the passed port config. Returns False if bridge doesn't exist. """ # drop all interface addresses first self.flush_addrs() for bridge, bridge_config in bridge_dict.items(): # add interface to bridge - use Section.klass to get BridgeIf class Section.klass(bridge)(bridge, create=True).add_port(self.ifname) # set bridge port path cost if 'cost' in bridge_config: self.set_path_cost(bridge_config['cost']) # set bridge port path priority if 'priority' in bridge_config: self.set_path_cost(bridge_config['priority']) def set_dhcp(self, enable): """ Enable/Disable DHCP client on a given interface. """ if enable not in [True, False]: raise ValueError() ifname = self.ifname config_base = r'/var/lib/dhcp/dhclient' config_file = f'{config_base}_{ifname}.conf' options_file = f'{config_base}_{ifname}.options' pid_file = f'{config_base}_{ifname}.pid' lease_file = f'{config_base}_{ifname}.leases' if enable and 'disable' not in self._config: if dict_search('dhcp_options.host_name', self._config) == None: # read configured system hostname. # maybe change to vyos hostd client ??? hostname = 'vyos' with open('/etc/hostname', 'r') as f: hostname = f.read().rstrip('\n') tmp = {'dhcp_options' : { 'host_name' : hostname}} self._config = dict_merge(tmp, self._config) render(options_file, 'dhcp-client/daemon-options.tmpl', - self._config, trim_blocks=True) + self._config) render(config_file, 'dhcp-client/ipv4.tmpl', - self._config, trim_blocks=True) + self._config) # 'up' check is mandatory b/c even if the interface is A/D, as soon as # the DHCP client is started the interface will be placed in u/u state. # This is not what we intended to do when disabling an interface. return self._cmd(f'systemctl restart dhclient@{ifname}.service') else: self._cmd(f'systemctl stop dhclient@{ifname}.service') # cleanup old config files for file in [config_file, options_file, pid_file, lease_file]: if os.path.isfile(file): os.remove(file) def set_dhcpv6(self, enable): """ Enable/Disable DHCPv6 client on a given interface. """ if enable not in [True, False]: raise ValueError() ifname = self.ifname config_file = f'/run/dhcp6c/dhcp6c.{ifname}.conf' if enable and 'disable' not in self._config: render(config_file, 'dhcp-client/ipv6.tmpl', - self._config, trim_blocks=True) + self._config) # We must ignore any return codes. This is required to enable DHCPv6-PD # for interfaces which are yet not up and running. return self._popen(f'systemctl restart dhcp6c@{ifname}.service') else: self._popen(f'systemctl stop dhcp6c@{ifname}.service') if os.path.isfile(config_file): os.remove(config_file) def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin on any interface. """ # Cache the configuration - it will be reused inside e.g. DHCP handler # XXX: maybe pass the option via __init__ in the future and rename this # method to apply()? self._config = config # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed. Skip if bond member. if 'is_bond_member' not in config: mac = config.get('hw_id') if 'mac' in config: mac = config.get('mac') if mac: self.set_mac(mac) # Update interface description self.set_alias(config.get('description', '')) # Ignore link state changes value = '2' if 'disable_link_detect' in config else '1' self.set_link_detect(value) # Configure assigned interface IP addresses. No longer # configured addresses will be removed first new_addr = config.get('address', []) # always ensure DHCP client is stopped (when not configured explicitly) if 'dhcp' not in new_addr: self.del_addr('dhcp') # always ensure DHCPv6 client is stopped (when not configured as client # for IPv6 address or prefix delegation dhcpv6pd = dict_search('dhcpv6_options.pd', config) if 'dhcpv6' not in new_addr or dhcpv6pd == None: self.del_addr('dhcpv6') # determine IP addresses which are assigned to the interface and build a # list of addresses which are no longer in the dict so they can be removed cur_addr = self.get_addr() for addr in list_diff(cur_addr, new_addr): self.del_addr(addr) for addr in new_addr: self.add_addr(addr) # start DHCPv6 client when only PD was configured if dhcpv6pd != None: self.set_dhcpv6(True) # There are some items in the configuration which can only be applied # if this instance is not bound to a bridge. This should be checked # by the caller but better save then sorry! if not any(k in ['is_bond_member', 'is_bridge_member'] for k in config): # Bind interface to given VRF or unbind it if vrf node is not set. # unbinding will call 'ip link set dev eth0 nomaster' which will # also drop the interface out of a bridge or bond - thus this is # checked before self.set_vrf(config.get('vrf', '')) # Configure ARP cache timeout in milliseconds - has default value tmp = dict_search('ip.arp_cache_timeout', config) value = tmp if (tmp != None) else '30' self.set_arp_cache_tmo(value) # Configure ARP filter configuration tmp = dict_search('ip.disable_arp_filter', config) value = '0' if (tmp != None) else '1' self.set_arp_filter(value) # Configure ARP accept tmp = dict_search('ip.enable_arp_accept', config) value = '1' if (tmp != None) else '0' self.set_arp_accept(value) # Configure ARP announce tmp = dict_search('ip.enable_arp_announce', config) value = '1' if (tmp != None) else '0' self.set_arp_announce(value) # Configure ARP ignore tmp = dict_search('ip.enable_arp_ignore', config) value = '1' if (tmp != None) else '0' self.set_arp_ignore(value) # Enable proxy-arp on this interface tmp = dict_search('ip.enable_proxy_arp', config) value = '1' if (tmp != None) else '0' self.set_proxy_arp(value) # Enable private VLAN proxy ARP on this interface tmp = dict_search('ip.proxy_arp_pvlan', config) value = '1' if (tmp != None) else '0' self.set_proxy_arp_pvlan(value) # IPv4 forwarding tmp = dict_search('ip.disable_forwarding', config) value = '0' if (tmp != None) else '1' self.set_ipv4_forwarding(value) # IPv6 forwarding tmp = dict_search('ipv6.disable_forwarding', config) value = '0' if (tmp != None) else '1' self.set_ipv6_forwarding(value) # IPv6 router advertisements tmp = dict_search('ipv6.address.autoconf', config) value = '2' if (tmp != None) else '1' if 'dhcpv6' in new_addr: value = '2' self.set_ipv6_accept_ra(value) # IPv6 address autoconfiguration tmp = dict_search('ipv6.address.autoconf', config) value = '1' if (tmp != None) else '0' self.set_ipv6_autoconf(value) # IPv6 Duplicate Address Detection (DAD) tries tmp = dict_search('ipv6.dup_addr_detect_transmits', config) value = tmp if (tmp != None) else '1' self.set_ipv6_dad_messages(value) # MTU - Maximum Transfer Unit if 'mtu' in config: self.set_mtu(config.get('mtu')) # Delete old IPv6 EUI64 addresses before changing MAC tmp = dict_search('ipv6.address.eui64_old', config) if tmp: for addr in tmp: self.del_ipv6_eui64_address(addr) # Manage IPv6 link-local addresses tmp = dict_search('ipv6.address.no_default_link_local', config) # we must check explicitly for None type as if the key is set we will # get an empty dict (<class 'dict'>) if tmp is not None: self.del_ipv6_eui64_address('fe80::/64') else: self.add_ipv6_eui64_address('fe80::/64') # Add IPv6 EUI-based addresses tmp = dict_search('ipv6.address.eui64', config) if tmp: for addr in tmp: self.add_ipv6_eui64_address(addr) # re-add ourselves to any bridge we might have fallen out of if 'is_bridge_member' in config: bridge_dict = config.get('is_bridge_member') self.add_to_bridge(bridge_dict) # remove no longer required 802.1ad (Q-in-Q VLANs) ifname = config['ifname'] for vif_s_id in config.get('vif_s_remove', {}): vif_s_ifname = f'{ifname}.{vif_s_id}' VLANIf(vif_s_ifname).remove() # create/update 802.1ad (Q-in-Q VLANs) for vif_s_id, vif_s_config in config.get('vif_s', {}).items(): tmp = deepcopy(VLANIf.get_config()) tmp['protocol'] = vif_s_config['protocol'] tmp['source_interface'] = ifname tmp['vlan_id'] = vif_s_id vif_s_ifname = f'{ifname}.{vif_s_id}' vif_s_config['ifname'] = vif_s_ifname s_vlan = VLANIf(vif_s_ifname, **tmp) s_vlan.update(vif_s_config) # remove no longer required client VLAN (vif-c) for vif_c_id in vif_s_config.get('vif_c_remove', {}): vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}' VLANIf(vif_c_ifname).remove() # create/update client VLAN (vif-c) interface for vif_c_id, vif_c_config in vif_s_config.get('vif_c', {}).items(): tmp = deepcopy(VLANIf.get_config()) tmp['source_interface'] = vif_s_ifname tmp['vlan_id'] = vif_c_id vif_c_ifname = f'{vif_s_ifname}.{vif_c_id}' vif_c_config['ifname'] = vif_c_ifname c_vlan = VLANIf(vif_c_ifname, **tmp) c_vlan.update(vif_c_config) # remove no longer required 802.1q VLAN interfaces for vif_id in config.get('vif_remove', {}): vif_ifname = f'{ifname}.{vif_id}' VLANIf(vif_ifname).remove() # create/update 802.1q VLAN interfaces for vif_id, vif_config in config.get('vif', {}).items(): tmp = deepcopy(VLANIf.get_config()) tmp['source_interface'] = ifname tmp['vlan_id'] = vif_id vif_ifname = f'{ifname}.{vif_id}' vif_config['ifname'] = vif_ifname vlan = VLANIf(vif_ifname, **tmp) vlan.update(vif_config) class VLANIf(Interface): """ Specific class which abstracts 802.1q and 802.1ad (Q-in-Q) VLAN interfaces """ default = { 'type': 'vlan', 'source_interface': '', 'vlan_id': '', 'protocol': '', 'ingress_qos': '', 'egress_qos': '', } options = Interface.options + \ ['source_interface', 'vlan_id', 'protocol', 'ingress_qos', 'egress_qos'] def remove(self): """ Remove interface from operating system. Removing the interface deconfigures all assigned IP addresses and clear possible DHCP(v6) client processes. Example: >>> from vyos.ifconfig import Interface >>> VLANIf('eth0.10').remove """ # Do we have sub interfaces (VLANs)? As interfaces need to be deleted # "in order" starting from Q-in-Q we delete them first. for upper in glob(f'/sys/class/net/{self.ifname}/upper*'): # an upper interface could be named: upper_bond0.1000.1100, thus # we need top drop the upper_ prefix vif_c = os.path.basename(upper) vif_c = vif_c.replace('upper_', '') VLANIf(vif_c).remove() super().remove() def _create(self): # bail out early if interface already exists if self.exists(f'{self.ifname}'): return cmd = 'ip link add link {source_interface} name {ifname} type vlan id {vlan_id}' if self.config['protocol']: cmd += ' protocol {protocol}' if self.config['ingress_qos']: cmd += ' ingress-qos-map {ingress_qos}' if self.config['egress_qos']: cmd += ' egress-qos-map {egress_qos}' self._cmd(cmd.format(**self.config)) # interface is always A/D down. It needs to be enabled explicitly self.set_admin_state('down') def set_admin_state(self, state): """ Set interface administrative state to be 'up' or 'down' Example: >>> from vyos.ifconfig import Interface >>> Interface('eth0.10').set_admin_state('down') >>> Interface('eth0.10').get_admin_state() 'down' """ # A VLAN interface can only be placed in admin up state when # the lower interface is up, too lower_interface = glob(f'/sys/class/net/{self.ifname}/lower*/flags')[0] with open(lower_interface, 'r') as f: flags = f.read() # If parent is not up - bail out as we can not bring up the VLAN. # Flags are defined in kernel source include/uapi/linux/if.h if not int(flags, 16) & 1: return None return super().set_admin_state(state) def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered interface setup code and provide a single point of entry when workin on any interface. """ # call base class first super().update(config) # Enable/Disable of an interface must always be done at the end of the # derived class to make use of the ref-counting set_admin_state() # function. We will only enable the interface if 'up' was called as # often as 'down'. This is required by some interface implementations # as certain parameters can only be changed when the interface is # in admin-down state. This ensures the link does not flap during # reconfiguration. state = 'down' if 'disable' in config else 'up' self.set_admin_state(state) diff --git a/python/vyos/template.py b/python/vyos/template.py index 7860b581f..b31f5bea2 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -1,217 +1,216 @@ # Copyright 2019-2020 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import functools import os from jinja2 import Environment from jinja2 import FileSystemLoader -from vyos.defaults import directories -from vyos.util import chmod, chown, makedir +from vyos.defaults import directories +from vyos.util import chmod +from vyos.util import chown +from vyos.util import makedir # Holds template filters registered via register_filter() _FILTERS = {} - -# reuse Environments with identical trim_blocks setting to improve performance +# reuse Environments with identical settings to improve performance @functools.lru_cache(maxsize=2) -def _get_environment(trim_blocks): +def _get_environment(): env = Environment( # Don't check if template files were modified upon re-rendering auto_reload=False, # Cache up to this number of templates for quick re-rendering cache_size=100, loader=FileSystemLoader(directories["templates"]), - trim_blocks=trim_blocks, + trim_blocks=True, ) env.filters.update(_FILTERS) return env def register_filter(name, func=None): """Register a function to be available as filter in templates under given name. It can also be used as a decorator, see below in this module for examples. :raise RuntimeError: when trying to register a filter after a template has been rendered already :raise ValueError: when trying to register a name which was taken already """ if func is None: return functools.partial(register_filter, name) if _get_environment.cache_info().currsize: raise RuntimeError( "Filters can only be registered before rendering the first template" ) if name in _FILTERS: raise ValueError(f"A filter with name {name!r} was registered already") _FILTERS[name] = func return func -def render_to_string(template, content, trim_blocks=False, formater=None): +def render_to_string(template, content, formater=None): """Render a template from the template directory, raise on any errors. :param template: the path to the template relative to the template folder :param content: the dictionary of variables to put into rendering context - :param trim_blocks: controls the trim_blocks jinja2 feature :param formater: if given, it has to be a callable the rendered string is passed through The parsed template files are cached, so rendering the same file multiple times does not cause as too much overhead. If used everywhere, it could be changed to load the template from Python environment variables from an importable Python module generated when the Debian package is build (recovering the load time and overhead caused by having the file out of the code). """ - template = _get_environment(bool(trim_blocks)).get_template(template) + template = _get_environment().get_template(template) rendered = template.render(content) if formater is not None: rendered = formater(rendered) return rendered def render( destination, template, content, - trim_blocks=False, formater=None, permission=None, user=None, group=None, ): """Render a template from the template directory to a file, raise on any errors. :param destination: path to the file to save the rendered template in :param permission: permission bitmask to set for the output file :param user: user to own the output file :param group: group to own the output file All other parameters are as for :func:`render_to_string`. """ # Create the directory if it does not exist folder = os.path.dirname(destination) makedir(folder, user, group) # As we are opening the file with 'w', we are performing the rendering before # calling open() to not accidentally erase the file if rendering fails - rendered = render_to_string(template, content, trim_blocks, formater) + rendered = render_to_string(template, content, formater) # Write to file with open(destination, "w") as file: chmod(file.fileno(), permission) chown(file.fileno(), user, group) file.write(rendered) ################################## # Custom template filters follow # ################################## @register_filter('address_from_cidr') def address_from_cidr(text): """ Take an IPv4/IPv6 CIDR prefix and convert the network to an "address". Example: 192.0.2.0/24 -> 192.0.2.0, 2001:db8::/48 -> 2001:db8:: """ from ipaddress import ip_network return str(ip_network(text).network_address) @register_filter('netmask_from_cidr') def netmask_from_cidr(text): """ Take CIDR prefix and convert the prefix length to a "subnet mask". Example: - 192.0.2.0/24 -> 255.255.255.0 - 2001:db8::/48 -> ffff:ffff:ffff:: """ from ipaddress import ip_network return str(ip_network(text).netmask) @register_filter('is_ip') def is_ip(addr): """ Check addr if it is an IPv4 or IPv6 address """ return is_ipv4(addr) or is_ipv6(addr) @register_filter('is_ipv4') def is_ipv4(text): """ Filter IP address, return True on IPv4 address, False otherwise """ from ipaddress import ip_interface try: return ip_interface(text).version == 4 except: return False @register_filter('is_ipv6') def is_ipv6(text): """ Filter IP address, return True on IPv6 address, False otherwise """ from ipaddress import ip_interface try: return ip_interface(text).version == 6 except: return False @register_filter('first_host_address') def first_host_address(text): """ Return first usable (host) IP address from given prefix. Example: - 10.0.0.0/24 -> 10.0.0.1 - 2001:db8::/64 -> 2001:db8:: """ from ipaddress import ip_interface from ipaddress import IPv4Network from ipaddress import IPv6Network addr = ip_interface(text) if addr.version == 4: return str(addr.ip +1) return str(addr.ip) @register_filter('last_host_address') def last_host_address(text): """ Return first usable IP address from given prefix. Example: - 10.0.0.0/24 -> 10.0.0.254 - 2001:db8::/64 -> 2001:db8::ffff:ffff:ffff:ffff """ from ipaddress import ip_interface from ipaddress import IPv4Network from ipaddress import IPv6Network addr = ip_interface(text) if addr.version == 4: return str(IPv4Network(addr).broadcast_address - 1) return str(IPv6Network(addr).broadcast_address) @register_filter('inc_ip') def inc_ip(address, increment): """ Increment given IP address by 'increment' Example (inc by 2): - 10.0.0.0/24 -> 10.0.0.2 - 2001:db8::/64 -> 2001:db8::2 """ from ipaddress import ip_interface return str(ip_interface(address).ip + int(increment)) @register_filter('dec_ip') def dec_ip(address, decrement): """ Decrement given IP address by 'decrement' Example (inc by 2): - 10.0.0.0/24 -> 10.0.0.2 - 2001:db8::/64 -> 2001:db8::2 """ from ipaddress import ip_interface return str(ip_interface(address).ip - int(decrement)) diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py index 78daeb6be..d93a2a8f4 100755 --- a/src/conf_mode/bcast_relay.py +++ b/src/conf_mode/bcast_relay.py @@ -1,111 +1,111 @@ #!/usr/bin/env python3 # # Copyright (C) 2017-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from glob import glob from netifaces import interfaces from sys import exit from vyos.config import Config from vyos.util import call from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() config_file_base = r'/etc/default/udp-broadcast-relay' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'broadcast-relay'] relay = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return relay def verify(relay): if not relay or 'disabled' in relay: return None for instance, config in relay.get('id', {}).items(): # we don't have to check this instance when it's disabled if 'disabled' in config: continue # we certainly require a UDP port to listen to if 'port' not in config: raise ConfigError(f'Port number mandatory for udp broadcast relay "{instance}"') # if only oone interface is given it's a string -> move to list if isinstance(config.get('interface', []), str): config['interface'] = [ config['interface'] ] # Relaying data without two interface is kinda senseless ... if len(config.get('interface', [])) < 2: raise ConfigError('At least two interfaces are required for udp broadcast relay "{instance}"') for interface in config.get('interface', []): if interface not in interfaces(): raise ConfigError('Interface "{interface}" does not exist!') return None def generate(relay): if not relay or 'disabled' in relay: return None for config in glob(config_file_base + '*'): os.remove(config) for instance, config in relay.get('id').items(): # we don't have to check this instance when it's disabled if 'disabled' in config: continue config['instance'] = instance render(config_file_base + instance, 'bcast-relay/udp-broadcast-relay.tmpl', - config, trim_blocks=True) + config) return None def apply(relay): # first stop all running services call('systemctl stop udp-broadcast-relay@*.service') if not relay or 'disable' in relay: return None # start only required service instances for instance, config in relay.get('id').items(): # we don't have to check this instance when it's disabled if 'disabled' in config: continue call(f'systemctl start udp-broadcast-relay@{instance}.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/dns_forwarding.py b/src/conf_mode/dns_forwarding.py index ef52cbfd3..c44e6c974 100755 --- a/src/conf_mode/dns_forwarding.py +++ b/src/conf_mode/dns_forwarding.py @@ -1,185 +1,185 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.hostsd_client import Client as hostsd_client from vyos.template import render from vyos.template import is_ipv6 from vyos.util import call from vyos.util import chown from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() pdns_rec_user = pdns_rec_group = 'pdns' pdns_rec_run_dir = '/run/powerdns' pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua' pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua' pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf' pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf' hostsd_tag = 'static' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'dns', 'forwarding'] if not conf.exists(base): return None dns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) dns = dict_merge(default_values, dns) # some additions to the default dictionary if 'system' in dns: base_nameservers = ['system', 'name-server'] if conf.exists(base_nameservers): dns.update({'system_name_server': conf.return_values(base_nameservers)}) base_nameservers_dhcp = ['system', 'name-servers-dhcp'] if conf.exists(base_nameservers_dhcp): dns.update({'system_name_server_dhcp': conf.return_values(base_nameservers_dhcp)}) # Split the source_address property into separate IPv4 and IPv6 lists # NOTE: In future versions of pdns-recursor (> 4.4.0), this logic can be removed # as both IPv4 and IPv6 addresses can be specified in a single setting. source_address_v4 = [] source_address_v6 = [] for source_address in dns['source_address']: if is_ipv6(source_address): source_address_v6.append(source_address) else: source_address_v4.append(source_address) dns.update({'source_address_v4': source_address_v4}) dns.update({'source_address_v6': source_address_v6}) return dns def verify(dns): # bail out early - looks like removal from running config if not dns: return None if 'listen_address' not in dns: raise ConfigError('DNS forwarding requires a listen-address') if 'allow_from' not in dns: raise ConfigError('DNS forwarding requires an allow-from network') # we can not use dict_search() when testing for domain servers # as a domain will contains dot's which is out dictionary delimiter. if 'domain' in dns: for domain in dns['domain']: if 'server' not in dns['domain'][domain]: raise ConfigError(f'No server configured for domain {domain}!') if 'system' in dns: if not ('system_name_server' in dns or 'system_name_server_dhcp' in dns): print("Warning: No 'system name-server' or 'system " \ "name-servers-dhcp' configured") return None def generate(dns): # bail out early - looks like removal from running config if not dns: return None render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl', - dns, trim_blocks=True, user=pdns_rec_user, group=pdns_rec_group) + dns, user=pdns_rec_user, group=pdns_rec_group) render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl', - dns, trim_blocks=True, user=pdns_rec_user, group=pdns_rec_group) + dns, user=pdns_rec_user, group=pdns_rec_group) # if vyos-hostsd didn't create its files yet, create them (empty) for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: with open(file, 'a'): pass chown(file, user=pdns_rec_user, group=pdns_rec_group) return None def apply(dns): if not dns: # DNS forwarding is removed in the commit call('systemctl stop pdns-recursor.service') if os.path.isfile(pdns_rec_config_file): os.unlink(pdns_rec_config_file) else: ### first apply vyos-hostsd config hc = hostsd_client() # add static nameservers to hostsd so they can be joined with other # sources hc.delete_name_servers([hostsd_tag]) if 'name_server' in dns: hc.add_name_servers({hostsd_tag: dns['name_server']}) # delete all nameserver tags hc.delete_name_server_tags_recursor(hc.get_name_server_tags_recursor()) ## add nameserver tags - the order determines the nameserver order! # our own tag (static) hc.add_name_server_tags_recursor([hostsd_tag]) if 'system' in dns: hc.add_name_server_tags_recursor(['system']) else: hc.delete_name_server_tags_recursor(['system']) # add dhcp nameserver tags for configured interfaces if 'system_name_server_dhcp' in dns: for interface in dns['system_name_server_dhcp']: hc.add_name_server_tags_recursor(['dhcp-' + interface, 'dhcpv6-' + interface ]) # hostsd will generate the forward-zones file # the list and keys() are required as get returns a dict, not list hc.delete_forward_zones(list(hc.get_forward_zones().keys())) if 'domain' in dns: hc.add_forward_zones(dns['domain']) # call hostsd to generate forward-zones and its lua-config-file hc.apply() ### finally (re)start pdns-recursor call('systemctl restart pdns-recursor.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 93e995b78..6d39c6644 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -1,155 +1,157 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import call from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/run/ddclient/ddclient.conf' # Mapping of service name to service protocol default_service_protocol = { 'afraid': 'freedns', 'changeip': 'changeip', 'cloudflare': 'cloudflare', 'dnspark': 'dnspark', 'dslreports': 'dslreports1', 'dyndns': 'dyndns2', 'easydns': 'easydns', 'namecheap': 'namecheap', 'noip': 'noip', 'sitelutions': 'sitelutions', 'zoneedit': 'zoneedit1' } def get_config(config=None): if config: conf = config else: conf = Config() base_level = ['service', 'dns', 'dynamic'] if not conf.exists(base_level): return None dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. for interface in dyndns['interface']: if 'service' in dyndns['interface'][interface]: # 'Autodetect' protocol used by DynDNS service for service in dyndns['interface'][interface]['service']: if service in default_service_protocol: dyndns['interface'][interface]['service'][service].update( {'protocol' : default_service_protocol.get(service)}) else: dyndns['interface'][interface]['service'][service].update( {'custom': ''}) if 'rfc2136' in dyndns['interface'][interface]: default_values = defaults(base_level + ['interface', 'rfc2136']) for rfc2136 in dyndns['interface'][interface]['rfc2136']: dyndns['interface'][interface]['rfc2136'][rfc2136] = dict_merge( default_values, dyndns['interface'][interface]['rfc2136'][rfc2136]) return dyndns def verify(dyndns): # bail out early - looks like removal from running config if not dyndns: return None # A 'node' corresponds to an interface if 'interface' not in dyndns: return None for interface in dyndns['interface']: # RFC2136 - configuration validation if 'rfc2136' in dyndns['interface'][interface]: for rfc2136, config in dyndns['interface'][interface]['rfc2136'].items(): for tmp in ['record', 'zone', 'server', 'key']: if tmp not in config: raise ConfigError(f'"{tmp}" required for rfc2136 based ' f'DynDNS service on "{interface}"') if not os.path.isfile(config['key']): raise ConfigError(f'"key"-file not found for rfc2136 based ' f'DynDNS service on "{interface}"') # DynDNS service provider - configuration validation if 'service' in dyndns['interface'][interface]: for service, config in dyndns['interface'][interface]['service'].items(): error_msg = f'required for DynDNS service "{service}" on "{interface}"' if 'host_name' not in config: raise ConfigError(f'"host-name" {error_msg}') if 'login' not in config: raise ConfigError(f'"login" (username) {error_msg}') if 'password' not in config: raise ConfigError(f'"password" {error_msg}') if 'zone' in config: if service != 'cloudflare': raise ConfigError(f'"zone" option only supported with CloudFlare') if 'custom' in config: if 'protocol' not in config: raise ConfigError(f'"protocol" {error_msg}') if 'server' not in config: raise ConfigError(f'"server" {error_msg}') return None def generate(dyndns): # bail out early - looks like removal from running config if not dyndns: return None - render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, trim_blocks=True, permission=0o600) + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns, + permission=0o600) + return None def apply(dyndns): if not dyndns: call('systemctl stop ddclient.service') if os.path.exists(config_file): os.unlink(config_file) else: call('systemctl restart ddclient.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index de228f0f8..a6e2d9c8c 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -1,180 +1,180 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import sys from copy import deepcopy import vyos.defaults import vyos.certbot_util from vyos.config import Config from vyos import ConfigError from vyos.util import call from vyos.template import render from vyos import airbag airbag.enable() config_file = '/etc/nginx/sites-available/default' certbot_dir = vyos.defaults.directories['certbot'] # https config needs to coordinate several subsystems: api, certbot, # self-signed certificate, as well as the virtual hosts defined within the # https config definition itself. Consequently, one needs a general dict, # encompassing the https and other configs, and a list of such virtual hosts # (server blocks in nginx terminology) to pass to the jinja2 template. default_server_block = { 'id' : '', 'address' : '*', 'port' : '443', 'name' : ['_'], 'api' : {}, 'vyos_cert' : {}, 'certbot' : False } def get_config(config=None): if config: conf = config else: conf = Config() if not conf.exists('service https'): return None server_block_list = [] https_dict = conf.get_config_dict('service https', get_first_key=True) # organize by vhosts vhost_dict = https_dict.get('virtual-host', {}) if not vhost_dict: # no specified virtual hosts (server blocks); use default server_block_list.append(default_server_block) else: for vhost in list(vhost_dict): server_block = deepcopy(default_server_block) server_block['id'] = vhost data = vhost_dict.get(vhost, {}) server_block['address'] = data.get('listen-address', '*') server_block['port'] = data.get('listen-port', '443') name = data.get('server-name', ['_']) server_block['name'] = name server_block_list.append(server_block) # get certificate data cert_dict = https_dict.get('certificates', {}) # self-signed certificate vyos_cert_data = {} if 'system-generated-certificate' in list(cert_dict): vyos_cert_data = vyos.defaults.vyos_cert_data if vyos_cert_data: for block in server_block_list: block['vyos_cert'] = vyos_cert_data # letsencrypt certificate using certbot certbot = False cert_domains = cert_dict.get('certbot', {}).get('domain-name', []) if cert_domains: certbot = True for domain in cert_domains: sub_list = vyos.certbot_util.choose_server_block(server_block_list, domain) if sub_list: for sb in sub_list: sb['certbot'] = True sb['certbot_dir'] = certbot_dir # certbot organizes certificates by first domain sb['certbot_domain_dir'] = cert_domains[0] # get api data api_set = False api_data = {} if 'api' in list(https_dict): api_set = True api_data = vyos.defaults.api_data api_settings = https_dict.get('api', {}) if api_settings: port = api_settings.get('port', '') if port: api_data['port'] = port vhosts = https_dict.get('api-restrict', {}).get('virtual-host', []) if vhosts: api_data['vhost'] = vhosts[:] if api_data: vhost_list = api_data.get('vhost', []) if not vhost_list: for block in server_block_list: block['api'] = api_data else: for block in server_block_list: if block['id'] in vhost_list: block['api'] = api_data # return dict for use in template https = {'server_block_list' : server_block_list, 'api_set': api_set, 'certbot': certbot} return https def verify(https): if https is None: return None if https['certbot']: for sb in https['server_block_list']: if sb['certbot']: return None raise ConfigError("At least one 'virtual-host <id> server-name' " "matching the 'certbot domain-name' is required.") return None def generate(https): if https is None: return None if 'server_block_list' not in https or not https['server_block_list']: https['server_block_list'] = [default_server_block] - render(config_file, 'https/nginx.default.tmpl', https, trim_blocks=True) + render(config_file, 'https/nginx.default.tmpl', https) return None def apply(https): if https is not None: call('systemctl restart nginx.service') else: call('systemctl stop nginx.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) sys.exit(1) diff --git a/src/conf_mode/igmp_proxy.py b/src/conf_mode/igmp_proxy.py index 90f3f30a8..fb030c9f3 100755 --- a/src/conf_mode/igmp_proxy.py +++ b/src/conf_mode/igmp_proxy.py @@ -1,122 +1,121 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from netifaces import interfaces from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import call from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/igmpproxy.conf' def get_config(config=None): if config: conf = config else: conf = Config() base = ['protocols', 'igmp-proxy'] igmp_proxy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) if 'interface' in igmp_proxy: # T2665: we must add the tagNode defaults individually until this is # moved to the base class default_values = defaults(base + ['interface']) for interface in igmp_proxy['interface']: igmp_proxy['interface'][interface] = dict_merge(default_values, igmp_proxy['interface'][interface]) if conf.exists(['protocols', 'igmp']): igmp_proxy.update({'igmp_configured': ''}) if conf.exists(['protocols', 'pim']): igmp_proxy.update({'pim_configured': ''}) return igmp_proxy def verify(igmp_proxy): # bail out early - looks like removal from running config if not igmp_proxy or 'disable' in igmp_proxy: return None if 'igmp_configured' in igmp_proxy or 'pim_configured' in igmp_proxy: raise ConfigError('Can not configure both IGMP proxy and PIM '\ 'at the same time') # at least two interfaces are required, one upstream and one downstream if 'interface' not in igmp_proxy or len(igmp_proxy['interface']) < 2: raise ConfigError('Must define exactly one upstream and at least one ' \ 'downstream interface!') upstream = 0 for interface, config in igmp_proxy['interface'].items(): if interface not in interfaces(): raise ConfigError(f'Interface "{interface}" does not exist') if dict_search('role', config) == 'upstream': upstream += 1 if upstream == 0: raise ConfigError('At least 1 upstream interface is required!') elif upstream > 1: raise ConfigError('Only 1 upstream interface allowed!') return None def generate(igmp_proxy): # bail out early - looks like removal from running config if not igmp_proxy: return None # bail out early - service is disabled, but inform user if 'disable' in igmp_proxy: print('WARNING: IGMP Proxy will be deactivated because it is disabled') return None - render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy, - trim_blocks=True) + render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy) return None def apply(igmp_proxy): if not igmp_proxy or 'disable' in igmp_proxy: # IGMP Proxy support is removed in the commit call('systemctl stop igmpproxy.service') if os.path.exists(config_file): os.unlink(config_file) else: call('systemctl restart igmpproxy.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 0e661c84b..25920f893 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -1,541 +1,540 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import re from sys import exit from ipaddress import IPv4Address from ipaddress import IPv4Network from ipaddress import IPv6Address from ipaddress import IPv6Network from ipaddress import summarize_address_range from netifaces import interfaces from shutil import rmtree from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_diffie_hellman_length from vyos.ifconfig import VTunIf from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call from vyos.util import chown from vyos.util import chmod_600 from vyos.util import dict_search from vyos.validate import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() user = 'openvpn' group = 'openvpn' cfg_file = '/run/openvpn/{ifname}.conf' def checkCertHeader(header, filename): """ Verify if filename contains specified header. Returns True if match is found, False if no match or file is not found """ if not os.path.isfile(filename): return False with open(filename, 'r') as f: for line in f: if re.match(header, line): return True return False def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'openvpn'] openvpn = get_interface_dict(conf, base) openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) openvpn['daemon_user'] = user openvpn['daemon_group'] = group return openvpn def verify(openvpn): if 'deleted' in openvpn: verify_bridge_delete(openvpn) return None if 'mode' not in openvpn: raise ConfigError('Must specify OpenVPN operation mode!') # Check if we have disabled ncp and at the same time specified ncp-ciphers if 'encryption' in openvpn: if {'disable_ncp', 'ncp_ciphers'} <= set(openvpn.get('encryption')): raise ConfigError('Can not specify both "encryption disable-ncp" '\ 'and "encryption ncp-ciphers"') # # OpenVPN client mode - VERIFY # if openvpn['mode'] == 'client': if 'local_port' in openvpn: raise ConfigError('Cannot specify "local-port" in client mode') if 'local_host' in openvpn: raise ConfigError('Cannot specify "local-host" in client mode') if 'remote_host' not in openvpn: raise ConfigError('Must specify "remote-host" in client mode') if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Protocol "tcp-passive" is not valid in client mode') if dict_search('tls.dh_file', openvpn): raise ConfigError('Cannot specify "tls dh-file" in client mode') # # OpenVPN site-to-site - VERIFY # elif openvpn['mode'] == 'site-to-site': if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "local-address" or add interface to bridge') if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1: raise ConfigError('Only one IPv4 local-address can be specified') if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1: raise ConfigError('Only one IPv6 local-address can be specified') if openvpn['device_type'] == 'tun': if 'remote_address' not in openvpn: raise ConfigError('Must specify "remote-address"') if 'remote_address' in openvpn: if len([addr for addr in openvpn['remote_address'] if is_ipv4(addr)]) > 1: raise ConfigError('Only one IPv4 remote-address can be specified') if len([addr for addr in openvpn['remote_address'] if is_ipv6(addr)]) > 1: raise ConfigError('Only one IPv6 remote-address can be specified') if not 'local_address' in openvpn: raise ConfigError('"remote-address" requires "local-address"') v4loAddr = [addr for addr in openvpn['local_address'] if is_ipv4(addr)] v4remAddr = [addr for addr in openvpn['remote_address'] if is_ipv4(addr)] if v4loAddr and not v4remAddr: raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address"') elif v4remAddr and not v4loAddr: raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"') v6remAddr = [addr for addr in openvpn['remote_address'] if is_ipv6(addr)] v6loAddr = [addr for addr in openvpn['local_address'] if is_ipv6(addr)] if v6loAddr and not v6remAddr: raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"') elif v6remAddr and not v6loAddr: raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"') if (v4loAddr == v4remAddr) or (v6remAddr == v4remAddr): raise ConfigError('"local-address" and "remote-address" cannot be the same') if dict_search('local_host', openvpn) in dict_search('local_address', openvpn): raise ConfigError('"local-address" cannot be the same as "local-host"') if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn): raise ConfigError('"remote-address" and "remote-host" can not be the same') if openvpn['device_type'] == 'tap': # we can only have one local_address, this is ensured above v4addr = None for laddr in openvpn['local_address']: if is_ipv4(laddr): v4addr = laddr break if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]: raise ConfigError('Must specify IPv4 "subnet-mask" for local-address') if dict_search('encryption.ncp_ciphers', openvpn): raise ConfigError('NCP ciphers can only be used in client or server mode') else: # checks for client-server or site-to-site bridged if 'local_address' in openvpn or 'remote_address' in openvpn: raise ConfigError('Cannot specify "local-address" or "remote-address" ' \ 'in client/server or bridge mode') # # OpenVPN server mode - VERIFY # if openvpn['mode'] == 'server': if openvpn['protocol'] == 'tcp-active': raise ConfigError('Protocol "tcp-active" is not valid in server mode') if 'remote_port' in openvpn: raise ConfigError('Cannot specify "remote-port" in server mode') if 'remote_host' in openvpn: raise ConfigError('Cannot specify "remote-host" in server mode') if 'tls' in openvpn: if 'dh_file' not in openvpn['tls']: if 'key_file' in openvpn['tls'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls']['key_file']): raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode') tmp = dict_search('server.subnet', openvpn) if tmp: v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)]) v6_subnets = len([subnet for subnet in tmp if is_ipv6(subnet)]) if v4_subnets > 1: raise ConfigError('Cannot specify more than 1 IPv4 server subnet') if v6_subnets > 1: raise ConfigError('Cannot specify more than 1 IPv6 server subnet') if v6_subnets > 0 and v4_subnets == 0: raise ConfigError('IPv6 server requires an IPv4 server subnet') for subnet in tmp: if is_ipv4(subnet): subnet = IPv4Network(subnet) if openvpn['device_type'] == 'tun' and subnet.prefixlen > 29: raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported') elif openvpn['device_type'] == 'tap' and subnet.prefixlen > 30: raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported') for client in (dict_search('client', openvpn) or []): if client['ip'] and not IPv4Address(client['ip'][0]) in subnet: raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}') else: if 'is_bridge_member' not in openvpn: raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode') for client in (dict_search('client', openvpn) or []): if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1: raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP') if dict_search('server.client_ip_pool', openvpn): if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)): raise ConfigError('Server client-ip-pool requires both start and stop addresses') else: v4PoolStart = IPv4Address(dict_search('server.client_ip_pool.start', openvpn)) v4PoolStop = IPv4Address(dict_search('server.client_ip_pool.stop', openvpn)) if v4PoolStart > v4PoolStop: raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}') v4PoolSize = int(v4PoolStop) - int(v4PoolStart) if v4PoolSize >= 65536: raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.') v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop)) for client in (dict_search('client', openvpn) or []): if client['ip']: for v4PoolNet in v4PoolNets: if IPv4Address(client['ip'][0]) in v4PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.') for subnet in (dict_search('server.subnet', openvpn) or []): if is_ipv6(subnet): tmp = dict_search('client_ipv6_pool.base', openvpn) if tmp: if not dict_search('server.client_ip_pool', openvpn): raise ConfigError('IPv6 server pool requires an IPv4 server pool') if int(tmp.split('/')[1]) >= 112: raise ConfigError('IPv6 server pool must be larger than /112') # # todo - weird logic # v6PoolStart = IPv6Address(tmp) v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536 if v6PoolSize < v4PoolSize: raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})') v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop)) for client in (dict_search('client', openvpn) or []): if client['ipv6_ip']: for v6PoolNet in v6PoolNets: if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet: print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.') else: for route in (dict_search('server.push_route', openvpn) or []): if is_ipv6(route): raise ConfigError('IPv6 push-route requires an IPv6 server subnet') #for client in openvpn ['client']: # if client['ipv6_ip']: # raise ConfigError(f'Server client "{client["name"]}" IPv6 IP requires an IPv6 server subnet') # if client['ipv6_push_route']: # raise ConfigError(f'Server client "{client["name"]} IPv6 push-route requires an IPv6 server subnet"') # if client['ipv6_subnet']: # raise ConfigError(f'Server client "{client["name"]} IPv6 subnet requires an IPv6 server subnet"') else: # checks for both client and site-to-site go here if dict_search('server.reject_unconfigured_clients', openvpn): raise ConfigError('Option reject-unconfigured-clients only supported in server mode') if 'replace_default_route' in openvpn and 'remote_host' not in openvpn: raise ConfigError('Cannot set "replace-default-route" without "remote-host"') # # OpenVPN common verification section # not depending on any operation mode # # verify specified IP address is present on any interface on this system if 'local_host' in openvpn: if not is_addr_assigned(openvpn['local_host']): raise ConfigError('local-host IP address "{local_host}" not assigned' \ ' to any interface'.format(**openvpn)) # TCP active if openvpn['protocol'] == 'tcp-active': if 'local_port' in openvpn: raise ConfigError('Cannot specify "local-port" with "tcp-active"') if 'remote_host' not in openvpn: raise ConfigError('Must specify "remote-host" with "tcp-active"') # shared secret and TLS if not ('shared_secret_key_file' in openvpn or 'tls' in openvpn): raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"') if {'shared_secret_key_file', 'tls'} <= set(openvpn): raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"') if openvpn['mode'] in ['client', 'server']: if 'tls' not in openvpn: raise ConfigError('Must specify "tls" for server and client mode') # # TLS/encryption # if 'shared_secret_key_file' in openvpn: if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']: raise ConfigError('GCM encryption with shared-secret-key-file not supported') file = dict_search('shared_secret_key_file', openvpn) if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file): raise ConfigError(f'Specified shared-secret-key-file "{file}" is not valid') if 'tls' in openvpn: if 'ca_cert_file' not in openvpn['tls']: raise ConfigError('Must specify "tls ca-cert-file"') if not (openvpn['mode'] == 'client' and 'auth_file' in openvpn['tls']): if 'cert_file' not in openvpn['tls']: raise ConfigError('Missing "tls cert-file"') if 'key_file' not in openvpn['tls']: raise ConfigError('Missing "tls key-file"') if {'auth_file', 'crypt_file'} <= set(openvpn['tls']): raise ConfigError('TLS auth and crypt are mutually exclusive') file = dict_search('tls.ca_cert_file', openvpn) if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file): raise ConfigError(f'Specified ca-cert-file "{file}" is invalid') file = dict_search('tls.auth_file', openvpn) if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file): raise ConfigError(f'Specified auth-file "{file}" is invalid') file = dict_search('tls.cert_file', openvpn) if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file): raise ConfigError(f'Specified cert-file "{file}" is invalid') file = dict_search('tls.key_file', openvpn) if file and not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', file): raise ConfigError(f'Specified key-file "{file}" is not valid') file = dict_search('tls.crypt_file', openvpn) if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file): raise ConfigError(f'Specified TLS crypt-file "{file}" is invalid') file = dict_search('tls.crl_file', openvpn) if file and not checkCertHeader('-----BEGIN X509 CRL-----', file): raise ConfigError(f'Specified crl-file "{file} not valid') file = dict_search('tls.dh_file', openvpn) if file and not checkCertHeader('-----BEGIN DH PARAMETERS-----', file): raise ConfigError(f'Specified dh-file "{file}" is not valid') if file and not verify_diffie_hellman_length(file, 2048): raise ConfigError(f'Minimum DH key-size is 2048 bits') tmp = dict_search('tls.role', openvpn) if tmp: if openvpn['mode'] in ['client', 'server']: if not dict_search('tls.auth_file', openvpn): raise ConfigError('Cannot specify "tls role" in client-server mode') if tmp == 'active': if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') if dict_search('tls.dh_file', openvpn): raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"') elif tmp == 'passive': if openvpn['protocol'] == 'tcp-active': raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') if not dict_search('tls.dh_file', openvpn): raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"') file = dict_search('tls.key_file', openvpn) if file and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', file): if dict_search('tls.dh_file', openvpn): print('Warning: using dh-file and EC keys simultaneously will ' \ 'lead to DH ciphers being used instead of ECDH') if dict_search('encryption.cipher', openvpn) == 'none': print('Warning: "encryption none" was specified!') print('No encryption will be performed and data is transmitted in ' \ 'plain text over the network!') # # Auth user/pass # if (dict_search('authentication.username', openvpn) and not dict_search('authentication.password', openvpn)): raise ConfigError('Password for authentication is missing') if (dict_search('authentication.password', openvpn) and not dict_search('authentication.username', openvpn)): raise ConfigError('Username for authentication is missing') verify_vrf(openvpn) return None def generate(openvpn): interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) # we can't know in advance which clients have been removed, # thus all client configs will be removed and re-added on demand ccd_dir = os.path.join(directory, 'ccd', interface) if os.path.isdir(ccd_dir): rmtree(ccd_dir, ignore_errors=True) if 'deleted' in openvpn or 'disable' in openvpn: return None # create client config directory on demand if not os.path.exists(ccd_dir): os.makedirs(ccd_dir, 0o755) chown(ccd_dir, user, group) # Fix file permissons for keys fix_permissions = [] tmp = dict_search('shared_secret_key_file', openvpn) if tmp: fix_permissions.append(openvpn['shared_secret_key_file']) tmp = dict_search('tls.key_file', openvpn) if tmp: fix_permissions.append(tmp) # Generate User/Password authentication file if 'authentication' in openvpn: render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.tmpl', openvpn, - trim_blocks=True, user=user, group=group, permission=0o600) + user=user, group=group, permission=0o600) else: # delete old auth file if present if os.path.isfile(openvpn['auth_user_pass_file']): os.remove(openvpn['auth_user_pass_file']) # Generate client specific configuration if dict_search('server.client', openvpn): for client, client_config in dict_search('server.client', openvpn).items(): client_file = os.path.join(ccd_dir, client) # Our client need's to know its subnet mask ... client_config['server_subnet'] = dict_search('server.subnet', openvpn) render(client_file, 'openvpn/client.conf.tmpl', client_config, - trim_blocks=True, user=user, group=group) + user=user, group=group) # we need to support quoting of raw parameters from OpenVPN CLI # see https://phabricator.vyos.net/T1632 render(cfg_file.format(**openvpn), 'openvpn/server.conf.tmpl', openvpn, - trim_blocks=True, formater=lambda _: _.replace(""", '"'), - user=user, group=group) + formater=lambda _: _.replace(""", '"'), user=user, group=group) # Fixup file permissions for file in fix_permissions: chmod_600(file) return None def apply(openvpn): interface = openvpn['ifname'] call(f'systemctl stop openvpn@{interface}.service') # Do some cleanup when OpenVPN is disabled/deleted if 'deleted' in openvpn or 'disable' in openvpn: # cleanup old configuration files cleanup = [] cleanup.append(cfg_file.format(**openvpn)) cleanup.append(openvpn['auth_user_pass_file']) for file in cleanup: if os.path.isfile(file): os.unlink(file) if interface in interfaces(): VTunIf(interface).remove() return None # No matching OpenVPN process running - maybe it got killed or none # existed - nevertheless, spawn new OpenVPN process call(f'systemctl start openvpn@{interface}.service') conf = VTunIf.get_config() conf['device_type'] = openvpn['device_type'] o = VTunIf(interface, **conf) o.update(openvpn) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index ee3b142c8..c31e49574 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -1,137 +1,137 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from copy import deepcopy from netifaces import interfaces from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_source_interface from vyos.configverify import verify_vrf from vyos.configverify import verify_mtu_ipv6 from vyos.template import render from vyos.util import call from vyos import ConfigError from vyos import airbag airbag.enable() def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'pppoe'] pppoe = get_interface_dict(conf, base) # PPPoE is "special" the default MTU is 1492 - update accordingly # as the config_level is already st in get_interface_dict() - we can use [] tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) if 'mtu' not in tmp: pppoe['mtu'] = '1492' return pppoe def verify(pppoe): if 'deleted' in pppoe: # bail out early return None verify_source_interface(pppoe) verify_vrf(pppoe) verify_mtu_ipv6(pppoe) if {'connect_on_demand', 'vrf'} <= set(pppoe): raise ConfigError('On-demand dialing and VRF can not be used at the same time') return None def generate(pppoe): # set up configuration file path variables where our templates will be # rendered into ifname = pppoe['ifname'] config_pppoe = f'/etc/ppp/peers/{ifname}' script_pppoe_pre_up = f'/etc/ppp/ip-pre-up.d/1000-vyos-pppoe-{ifname}' script_pppoe_ip_up = f'/etc/ppp/ip-up.d/1000-vyos-pppoe-{ifname}' script_pppoe_ip_down = f'/etc/ppp/ip-down.d/1000-vyos-pppoe-{ifname}' script_pppoe_ipv6_up = f'/etc/ppp/ipv6-up.d/1000-vyos-pppoe-{ifname}' config_wide_dhcp6c = f'/run/dhcp6c/dhcp6c.{ifname}.conf' config_files = [config_pppoe, script_pppoe_pre_up, script_pppoe_ip_up, script_pppoe_ip_down, script_pppoe_ipv6_up, config_wide_dhcp6c] if 'deleted' in pppoe: # stop DHCPv6-PD client call(f'systemctl stop dhcp6c@{ifname}.service') # Hang-up PPPoE connection call(f'systemctl stop ppp@{ifname}.service') # Delete PPP configuration files for file in config_files: if os.path.exists(file): os.unlink(file) return None # Create PPP configuration files - render(config_pppoe, 'pppoe/peer.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(config_pppoe, 'pppoe/peer.tmpl', pppoe, permission=0o755) + # Create script for ip-pre-up.d - render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl', pppoe, + permission=0o755) # Create script for ip-up.d - render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl', pppoe, + permission=0o755) # Create script for ip-down.d - render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl', pppoe, + permission=0o755) # Create script for ipv6-up.d - render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', - pppoe, trim_blocks=True, permission=0o755) + render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl', pppoe, + permission=0o755) if 'dhcpv6_options' in pppoe and 'pd' in pppoe['dhcpv6_options']: # ipv6.tmpl relies on ifname - this should be made consitent in the # future better then double key-ing the same value - render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe, trim_blocks=True) + render(config_wide_dhcp6c, 'dhcp-client/ipv6.tmpl', pppoe) return None def apply(pppoe): if 'deleted' in pppoe: # bail out early return None if 'disable' not in pppoe: # Dial PPPoE connection call('systemctl restart ppp@{ifname}.service'.format(**pppoe)) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index d302c7df7..b25fcd4e0 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -1,290 +1,292 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from re import findall from netaddr import EUI, mac_unix_expanded from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import dict_merge from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_dhcpv6 from vyos.configverify import verify_source_interface from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.ifconfig import WiFiIf from vyos.template import render from vyos.util import call from vyos.util import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() # XXX: wpa_supplicant works on the source interface wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' hostapd_conf = '/run/hostapd/{ifname}.conf' def find_other_stations(conf, base, ifname): """ Only one wireless interface per phy can be in station mode - find all interfaces attached to a phy which run in station mode """ old_level = conf.get_level() conf.set_level(base) dict = {} for phy in os.listdir('/sys/class/ieee80211'): list = [] for interface in conf.list_nodes([]): if interface == ifname: continue # the following node is mandatory if conf.exists([interface, 'physical-device', phy]): tmp = conf.return_value([interface, 'type']) if tmp == 'station': list.append(interface) if list: dict.update({phy: list}) conf.set_level(old_level) return dict def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'wireless'] wifi = get_interface_dict(conf, base) # Cleanup "delete" default values when required user selectable values are # not defined at all tmp = conf.get_config_dict([], key_mangling=('-', '_'), get_first_key=True) if not (dict_search('security.wpa.passphrase', tmp) or dict_search('security.wpa.radius', tmp)): del wifi['security']['wpa'] # defaults include RADIUS server specifics per TAG node which need to be # added to individual RADIUS servers instead - so we can simply delete them if dict_search('security.wpa.radius.server.port', wifi): del wifi['security']['wpa']['radius']['server']['port'] if not len(wifi['security']['wpa']['radius']['server']): del wifi['security']['wpa']['radius'] if not len(wifi['security']['wpa']): del wifi['security']['wpa'] if not len(wifi['security']): del wifi['security'] if 'security' in wifi and 'wpa' in wifi['security']: wpa_cipher = wifi['security']['wpa'].get('cipher') wpa_mode = wifi['security']['wpa'].get('mode') if not wpa_cipher: tmp = None if wpa_mode == 'wpa': tmp = {'security': {'wpa': {'cipher' : ['TKIP', 'CCMP']}}} elif wpa_mode == 'wpa2': tmp = {'security': {'wpa': {'cipher' : ['CCMP']}}} elif wpa_mode == 'both': tmp = {'security': {'wpa': {'cipher' : ['CCMP', 'TKIP']}}} if tmp: wifi = dict_merge(tmp, wifi) # Only one wireless interface per phy can be in station mode tmp = find_other_stations(conf, base, wifi['ifname']) if tmp: wifi['station_interfaces'] = tmp # Add individual RADIUS server default values if dict_search('security.wpa.radius.server', wifi): default_values = defaults(base + ['security', 'wpa', 'radius', 'server']) for server in dict_search('security.wpa.radius.server', wifi): wifi['security']['wpa']['radius']['server'][server] = dict_merge( default_values, wifi['security']['wpa']['radius']['server'][server]) return wifi def verify(wifi): if 'deleted' in wifi: verify_bridge_delete(wifi) return None if 'physical_device' not in wifi: raise ConfigError('You must specify a physical-device "phy"') if 'type' not in wifi: raise ConfigError('You must specify a WiFi mode') if 'ssid' not in wifi and wifi['type'] != 'monitor': raise ConfigError('SSID must be configured') if wifi['type'] == 'access-point': if 'country_code' not in wifi: raise ConfigError('Wireless country-code is mandatory') if 'channel' not in wifi: raise ConfigError('Wireless channel must be configured!') if 'security' in wifi: if {'wep', 'wpa'} <= set(wifi.get('security', {})): raise ConfigError('Must either use WEP or WPA security!') if 'wep' in wifi['security']: if 'key' in wifi['security']['wep'] and len(wifi['security']['wep']) > 4: raise ConfigError('No more then 4 WEP keys configurable') elif 'key' not in wifi['security']['wep']: raise ConfigError('Security WEP configured - missing WEP keys!') elif 'wpa' in wifi['security']: wpa = wifi['security']['wpa'] if not any(i in ['passphrase', 'radius'] for i in wpa): raise ConfigError('Misssing WPA key or RADIUS server') if 'radius' in wpa: if 'server' in wpa['radius']: for server in wpa['radius']['server']: if 'key' not in wpa['radius']['server'][server]: raise ConfigError(f'Misssing RADIUS shared secret key for server: {server}') if 'capabilities' in wifi: capabilities = wifi['capabilities'] if 'vht' in capabilities: if 'ht' not in capabilities: raise ConfigError('Specify HT flags if you want to use VHT!') if {'beamform', 'antenna_count'} <= set(capabilities.get('vht', {})): if capabilities['vht']['antenna_count'] == '1': raise ConfigError('Cannot use beam forming with just one antenna!') if capabilities['vht']['beamform'] == 'single-user-beamformer': if int(capabilities['vht']['antenna_count']) < 3: # Nasty Gotcha: see https://w1.fi/cgit/hostap/plain/hostapd/hostapd.conf lines 692-705 raise ConfigError('Single-user beam former requires at least 3 antennas!') if 'station_interfaces' in wifi and wifi['type'] == 'station': phy = wifi['physical_device'] if phy in wifi['station_interfaces']: if len(wifi['station_interfaces'][phy]) > 0: raise ConfigError('Only one station per wireless physical interface possible!') verify_address(wifi) verify_vrf(wifi) # use common function to verify VLAN configuration verify_vlan_config(wifi) return None def generate(wifi): interface = wifi['ifname'] # always stop hostapd service first before reconfiguring it call(f'systemctl stop hostapd@{interface}.service') # always stop wpa_supplicant service first before reconfiguring it call(f'systemctl stop wpa_supplicant@{interface}.service') # Delete config files if interface is removed if 'deleted' in wifi: if os.path.isfile(hostapd_conf.format(**wifi)): os.unlink(hostapd_conf.format(**wifi)) if os.path.isfile(wpa_suppl_conf.format(**wifi)): os.unlink(wpa_suppl_conf.format(**wifi)) return None if 'mac' not in wifi: # http://wiki.stocksy.co.uk/wiki/Multiple_SSIDs_with_hostapd # generate locally administered MAC address from used phy interface with open('/sys/class/ieee80211/{physical_device}/addresses'.format(**wifi), 'r') as f: # some PHYs tend to have multiple interfaces and thus supply multiple MAC # addresses - we only need the first one for our calculation tmp = f.readline().rstrip() tmp = EUI(tmp).value # mask last nibble from the MAC address tmp &= 0xfffffffffff0 # set locally administered bit in MAC address tmp |= 0x020000000000 # we now need to add an offset to our MAC address indicating this # subinterfaces index tmp += int(findall(r'\d+', interface)[0]) # convert integer to "real" MAC address representation mac = EUI(hex(tmp).split('x')[-1]) # change dialect to use : as delimiter instead of - mac.dialect = mac_unix_expanded wifi['mac'] = str(mac) # XXX: Jinja2 can not operate on a dictionary key when it starts of with a number if '40mhz_incapable' in (dict_search('capabilities.ht', wifi) or []): wifi['capabilities']['ht']['fourtymhz_incapable'] = wifi['capabilities']['ht']['40mhz_incapable'] del wifi['capabilities']['ht']['40mhz_incapable'] # render appropriate new config files depending on access-point or station mode if wifi['type'] == 'access-point': - render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', wifi, trim_blocks=True) + render(hostapd_conf.format(**wifi), 'wifi/hostapd.conf.tmpl', + wifi) elif wifi['type'] == 'station': - render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True) + render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', + wifi) return None def apply(wifi): interface = wifi['ifname'] if 'deleted' in wifi: WiFiIf(interface).remove() else: # This is a special type of interface which needs additional parameters # when created using iproute2. Instead of passing a ton of arguments, # use a dictionary provided by the interface class which holds all the # options necessary. conf = WiFiIf.get_config() # Assign WiFi instance configuration parameters to config dict conf['phy'] = wifi['physical_device'] # Finally create the new interface w = WiFiIf(interface, **conf) w.update(wifi) # Enable/Disable interface - interface is always placed in # administrative down state in WiFiIf class if 'disable' not in wifi: # Physical interface is now configured. Proceed by starting hostapd or # wpa_supplicant daemon. When type is monitor we can just skip this. if wifi['type'] == 'access-point': call(f'systemctl start hostapd@{interface}.service') elif wifi['type'] == 'station': call(f'systemctl start wpa_supplicant@{interface}.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index bce3405d0..976953b31 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -1,132 +1,132 @@ #!/usr/bin/env python3 # # Copyright (C) 2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.template import render from vyos.util import call from vyos.util import check_kmod from vyos.util import find_device_file from vyos import ConfigError from vyos import airbag airbag.enable() k_mod = ['option', 'usb_wwan', 'usbserial'] def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the interface name will be added or a deleted flag """ if config: conf = config else: conf = Config() base = ['interfaces', 'wirelessmodem'] wwan = get_interface_dict(conf, base) return wwan def verify(wwan): if 'deleted' in wwan: return None if not 'apn' in wwan: raise ConfigError('No APN configured for "{ifname}"'.format(**wwan)) if not 'device' in wwan: raise ConfigError('Physical "device" must be configured') # we can not use isfile() here as Linux device files are no regular files # thus the check will return False dev_path = find_device_file(wwan['device']) if dev_path is None or not os.path.exists(dev_path): raise ConfigError('Device "{device}" does not exist'.format(**wwan)) verify_vrf(wwan) return None def generate(wwan): # set up configuration file path variables where our templates will be # rendered into ifname = wwan['ifname'] config_wwan = f'/etc/ppp/peers/{ifname}' config_wwan_chat = f'/etc/ppp/peers/chat.{ifname}' script_wwan_pre_up = f'/etc/ppp/ip-pre-up.d/1010-vyos-wwan-{ifname}' script_wwan_ip_up = f'/etc/ppp/ip-up.d/1010-vyos-wwan-{ifname}' script_wwan_ip_down = f'/etc/ppp/ip-down.d/1010-vyos-wwan-{ifname}' config_files = [config_wwan, config_wwan_chat, script_wwan_pre_up, script_wwan_ip_up, script_wwan_ip_down] # Always hang-up WWAN connection prior generating new configuration file call(f'systemctl stop ppp@{ifname}.service') if 'deleted' in wwan: # Delete PPP configuration files for file in config_files: if os.path.exists(file): os.unlink(file) else: wwan['device'] = find_device_file(wwan['device']) # Create PPP configuration files - render(config_wwan, 'wwan/peer.tmpl', wwan, trim_blocks=True) + render(config_wwan, 'wwan/peer.tmpl', wwan) # Create PPP chat script - render(config_wwan_chat, 'wwan/chat.tmpl', wwan, trim_blocks=True) + 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, trim_blocks=True, permission=0o755) + wwan, permission=0o755) # Create script for ip-up.d render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl', - wwan, trim_blocks=True, permission=0o755) + wwan, permission=0o755) # Create script for ip-down.d render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl', - wwan, trim_blocks=True, permission=0o755) + wwan, permission=0o755) return None def apply(wwan): if 'deleted' in wwan: # bail out early return None if not 'disable' in wwan: # "dial" WWAN connection call('systemctl start ppp@{ifname}.service'.format(**wwan)) return None if __name__ == '__main__': try: check_kmod(k_mod) c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 11a5b7aaa..a65e8b567 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -1,230 +1,230 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import re import os from time import sleep from sys import exit from vyos.config import Config from vyos import ConfigError from vyos.util import call from vyos.template import render from vyos import airbag airbag.enable() ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" ipsec_secrets_file = "/etc/ipsec.secrets" ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/" ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name ipsec_conf_file = "/etc/ipsec.conf" ca_cert_path = "/etc/ipsec.d/cacerts" server_cert_path = "/etc/ipsec.d/certs" server_key_path = "/etc/ipsec.d/private" delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" charon_pidfile = "/var/run/charon.pid" def get_config(config=None): if config: config = config else: config = Config() data = {"install_routes": "yes"} if config.exists("vpn ipsec options disable-route-autoinstall"): data["install_routes"] = "no" if config.exists("vpn ipsec ipsec-interfaces interface"): data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface") # Init config variables data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end data["ipsec_ra_conn_file"] = ipsec_ra_conn_file data["ra_conn_name"] = ra_conn_name # Get l2tp ipsec settings data["ipsec_l2tp"] = False conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful if config.exists(conf_ipsec_command): data["ipsec_l2tp"] = True # Authentication params if config.exists(conf_ipsec_command + "authentication mode"): data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode") if config.exists(conf_ipsec_command + "authentication pre-shared-secret"): data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret") # mode x509 if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"): data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file") if config.exists(conf_ipsec_command + "authentication x509 crl-file"): data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file") if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"): data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file") data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0) if config.exists(conf_ipsec_command + "authentication x509 server-key-file"): data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file") data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0) if config.exists(conf_ipsec_command + "authentication x509 server-key-password"): data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password") # Common l2tp ipsec params if config.exists(conf_ipsec_command + "ike-lifetime"): data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime") else: data["ipsec_l2tp_ike_lifetime"] = "3600" if config.exists(conf_ipsec_command + "lifetime"): data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime") else: data["ipsec_l2tp_lifetime"] = "3600" if config.exists("vpn l2tp remote-access outside-address"): data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address') return data 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 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) with open(ipsec_secrets_file, 'a+') as f: f.write(secret_txt) os.umask(old_umask) def write_ipsec_conf(c): ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end) old_umask = os.umask(0o077) with open(ipsec_conf_file, 'a+') as f: f.write(ipsec_confg_txt) os.umask(old_umask) ### Remove config from file by delimiter def remove_confs(delim_begin, delim_end, conf_file): call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) ### Checking certificate storage and notice if certificate not in /config directory def check_cert_file_store(cert_name, file_path, dts_path): if not re.search('^\/config\/.+', file_path): print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.") #Checking file existence if not os.path.isfile(file_path): raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"") else: ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ # todo make check ret = call('cp -f '+file_path+' '+dts_path) if ret: raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) def verify(data): # l2tp ipsec check if data["ipsec_l2tp"]: # Checking dependecies for "authentication mode pre-shared-secret" if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": if not data.get("ipsec_l2tp_secret"): raise ConfigError("pre-shared-secret required") if not data.get("outside_addr"): raise ConfigError("outside-address not defined") # Checking dependecies for "authentication mode x509" if data.get("ipsec_l2tp_auth_mode") == "x509": if not data.get("ipsec_l2tp_x509_server_key_file"): raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.") else: check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path) if not data.get("ipsec_l2tp_x509_server_cert_file"): raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.") else: check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path) if not data.get("ipsec_l2tp_x509_ca_cert_file"): raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509") else: check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path) if not data.get('ipsec_interfaces'): raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") def generate(data): - render(charon_conf_file, 'ipsec/charon.tmpl', data, trim_blocks=True) + render(charon_conf_file, 'ipsec/charon.tmpl', data) if data["ipsec_l2tp"]: remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) # old_umask = os.umask(0o077) - # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data, trim_blocks=True) + # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data) # os.umask(old_umask) ## Use this method while IPSec CLI handler won't be overwritten to python write_ipsec_secrets(data) old_umask = os.umask(0o077) # Create tunnels directory if does not exist if not os.path.exists(ipsec_ra_conn_dir): os.makedirs(ipsec_ra_conn_dir) - render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data, trim_blocks=True) + render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data) os.umask(old_umask) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) # old_umask = os.umask(0o077) - # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data, trim_blocks=True) + # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data) # os.umask(old_umask) ## Use this method while IPSec CLI handler won't be overwritten to python write_ipsec_conf(data) else: if os.path.exists(ipsec_ra_conn_file): remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) def restart_ipsec(): call('ipsec restart >&/dev/null') # counter for apply swanctl config counter = 10 while counter <= 10: if os.path.exists(charon_pidfile): call('swanctl -q >&/dev/null') break counter -=1 sleep(1) if counter == 0: raise ConfigError('VPN configuration error: IPSec is not running.') def apply(data): # Restart IPSec daemon restart_ipsec() if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index b66cd370a..b467f3d74 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -1,290 +1,291 @@ #!/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 distutils.version import LooseVersion from platform import release as kernel_version from sys import exit from netifaces import interfaces from vyos.config import Config from vyos.template import render from vyos.util import call from vyos.util import cmd from vyos.util import check_kmod from vyos.validate import is_addr_assigned from vyos import ConfigError from vyos import airbag airbag.enable() if LooseVersion(kernel_version()) > LooseVersion('5.1'): k_mod = ['nft_nat', 'nft_chain_nat'] else: k_mod = ['nft_nat', 'nft_chain_nat_ipv4'] 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 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') def parse_configuration(conf, source_dest): """ Common wrapper to read in both NAT source and destination CLI """ ruleset = [] 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']): tmp = conf.return_value(['destination', 'address']) if tmp.startswith('!'): tmp = tmp.replace('!', '!=') rule['dest_address'] = tmp if conf.exists(['destination', 'port']): tmp = conf.return_value(['destination', 'port']) if tmp.startswith('!'): tmp = tmp.replace('!', '!=') rule['dest_port'] = tmp 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']): tmp = conf.return_value(['source', 'address']) if tmp.startswith('!'): tmp = tmp.replace('!', '!=') rule['source_address'] = tmp if conf.exists(['source', 'prefix']): rule['source_prefix'] = conf.return_value(['source', 'prefix']) if conf.exists(['source', 'port']): tmp = conf.return_value(['source', 'port']) if tmp.startswith('!'): tmp = tmp.replace('!', '!=') rule['source_port'] = tmp 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']) ruleset.append(rule) return ruleset def get_config(config=None): nat = deepcopy(default_config_data) if config: conf = config else: 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_configuration(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 'any' and interface not in interfaces(): print(f'Warning: rule "{rule["number"]}" 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': for ip in addr.split('-'): if not is_addr_assigned(ip): print(f'Warning: IP address {ip} does not exist on the system!') elif not rule['exclude']: raise ConfigError(f'{err_msg} translation address not specified') # 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 'any' and interface not in interfaces(): print(f'Warning: rule "{rule["number"]}" interface "{interface}" does not exist on this system') if not rule['interface_in']: raise ConfigError(f'{err_msg} inbound-interface not specified') if not rule['translation_address'] and not rule['exclude']: raise ConfigError(f'{err_msg} translation address 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) + render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, + 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(k_mod) c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/ntp.py b/src/conf_mode/ntp.py index d6453ec83..b102b3e9e 100755 --- a/src/conf_mode/ntp.py +++ b/src/conf_mode/ntp.py @@ -1,85 +1,85 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from vyos.config import Config from vyos.configverify import verify_vrf from vyos import ConfigError from vyos.util import call from vyos.template import render from vyos import airbag airbag.enable() config_file = r'/etc/ntp.conf' systemd_override = r'/etc/systemd/system/ntp.service.d/override.conf' def get_config(config=None): if config: conf = config else: conf = Config() base = ['system', 'ntp'] ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return ntp def verify(ntp): # bail out early - looks like removal from running config if not ntp: return None if len(ntp.get('allow_clients', {})) and not (len(ntp.get('server', {})) > 0): raise ConfigError('NTP server not configured') verify_vrf(ntp) return None def generate(ntp): # bail out early - looks like removal from running config if not ntp: return None - render(config_file, 'ntp/ntp.conf.tmpl', ntp, trim_blocks=True) - render(systemd_override, 'ntp/override.conf.tmpl', ntp, trim_blocks=True) + render(config_file, 'ntp/ntp.conf.tmpl', ntp) + render(systemd_override, 'ntp/override.conf.tmpl', ntp) return None def apply(ntp): if not ntp: # NTP support is removed in the commit call('systemctl stop ntp.service') if os.path.exists(config_file): os.unlink(config_file) if os.path.isfile(systemd_override): os.unlink(systemd_override) # Reload systemd manager configuration call('systemctl daemon-reload') if ntp: call('systemctl restart ntp.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 981ff9fe9..642738b09 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -1,121 +1,119 @@ #!/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/>. from sys import exit from vyos.config import Config from vyos.util import call from vyos.util import dict_search from vyos.template import render from vyos.template import render_to_string from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() config_file = r'/tmp/bgp.frr' def get_config(): conf = Config() base = ['protocols', 'nbgp'] bgp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # XXX: any reason we can not move this into the FRR template? # we shall not call vtysh directly, especially not in get_config() if not conf.exists(base): bgp = {} call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ') if not conf.exists(base + ['route-map']): call('vtysh -c \"conf t\" -c \"no ip protocol bgp\" ') return bgp def verify(bgp): if not bgp: return None # Check if declared more than one ASN if len(bgp) > 1: raise ConfigError('Only one BGP AS can be defined!') for asn, asn_config in bgp.items(): # Common verification for both peer-group and neighbor statements for neigh in ['neighbor', 'peer_group']: # bail out early if there is no neighbor or peer-group statement # this also saves one indention level if neigh not in asn_config: continue for neighbor, config in asn_config[neigh].items(): if 'remote_as' not in config and 'peer_group' not in config: raise ConfigError(f'BGP remote-as must be specified for "{neighbor}"!') if 'remote_as' in config and 'peer_group' in config: raise ConfigError(f'BGP peer-group member "{neighbor}" cannot override remote-as of peer-group!') return None def generate(bgp): if not bgp: bgp['new_frr_config'] = '' return None # only one BGP AS is supported, so we can directly send the first key # of the config dict asn = list(bgp.keys())[0] bgp[asn]['asn'] = asn # render(config) not needed, its only for debug - render(config_file, 'frr/bgp.frr.tmpl', bgp[asn], trim_blocks=True) - - bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp[asn], - trim_blocks=True) + render(config_file, 'frr/bgp.frr.tmpl', bgp[asn]) + bgp['new_frr_config'] = render_to_string('frr/bgp.frr.tmpl', bgp[asn]) return None def apply(bgp): # Save original configuration prior to starting any commit actions frr_cfg = {} frr_cfg['original_config'] = frr.get_configuration(daemon='bgpd') frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], bgp['new_frr_config'], from_re='router bgp .*') # Debugging print('') print('--------- DEBUGGING ----------') print(f'Existing config:\n{frr_cfg["original_config"]}\n\n') print(f'Replacement config:\n{bgp["new_frr_config"]}\n\n') print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n') # FRR mark configuration will test for syntax errors and throws an # exception if any syntax errors is detected frr.mark_configuration(frr_cfg['modified_config']) # Commit resulting configuration to FRR, this will throw CommitError # on failure frr.reload_configuration(frr_cfg['modified_config'], daemon='bgpd') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 03e11c6c4..df03fd990 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,148 +1,148 @@ #!/usr/bin/env python3 # # Copyright (C) 2017-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import node_changed from vyos import ConfigError from vyos.util import call from vyos.template import render from vyos.template import render_to_string from vyos import frr from vyos import airbag airbag.enable() config_file = r'/tmp/isis.frr' def get_config(config=None): if config: conf = config else: conf = Config() base = ['protocols', 'isis'] isis = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # determine which members have been removed for instance in isis: conf.set_level(base + [instance]) tmp = node_changed(conf, ['interface']) if tmp: isis[instance].update({'interface_remove': tmp}) return isis def verify(isis): # bail out early - looks like removal from running config if not isis: return None for process, isis_config in isis.items(): # If more then one isis process is defined (Frr only supports one) # http://docs.frrouting.org/en/latest/isisd.html#isis-router if len(isis) > 1: raise ConfigError('Only one isis process can be definded') # If network entity title (net) not defined if 'net' not in isis_config: raise ConfigError('ISIS net format iso is mandatory!') # If interface not set if 'interface' not in isis_config: raise ConfigError('ISIS interface is mandatory!') # If md5 and plaintext-password set at the same time if 'area_password' in isis_config: if {'md5', 'plaintext_password'} <= set(isis_config['encryption']): raise ConfigError('Can not use both md5 and plaintext-password for ISIS area-password!') # If one param from deley set, but not set others if 'spf_delay_ietf' in isis_config: required_timers = ['holddown', 'init_delay', 'long_delay', 'short_delay', 'time_to_learn'] exist_timers = [] for elm_timer in required_timers: if elm_timer in isis_config['spf_delay_ietf']: exist_timers.append(elm_timer) exist_timers = set(required_timers).difference(set(exist_timers)) if len(exist_timers) > 0: raise ConfigError('All types of delay must be specified: ' + ', '.join(exist_timers).replace('_', '-')) # If Redistribute set, but level don't set if 'redistribute' in isis_config: proc_level = isis_config.get('level','').replace('-','_') for proto, proto_config in isis_config.get('redistribute', {}).get('ipv4', {}).items(): if 'level_1' not in proto_config and 'level_2' not in proto_config: raise ConfigError('Redistribute level-1 or level-2 should be specified in \"protocols isis {} redistribute ipv4 {}\"'.format(process, proto)) for redistribute_level in proto_config.keys(): if proc_level and proc_level != 'level_1_2' and proc_level != redistribute_level: raise ConfigError('\"protocols isis {0} redistribute ipv4 {2} {3}\" cannot be used with \"protocols isis {0} level {1}\"'.format(process, proc_level, proto, redistribute_level)) return None def generate(isis): if not isis: isis['new_frr_config'] = '' return None # only one ISIS process is supported, so we can directly send the first key # of the config dict process = list(isis.keys())[0] isis[process]['process'] = process # render(config) not needed, its only for debug - render(config_file, 'frr/isis.frr.tmpl', isis[process], trim_blocks=True) + render(config_file, 'frr/isis.frr.tmpl', isis[process]) isis['new_frr_config'] = render_to_string('frr/isis.frr.tmpl', - isis[process], trim_blocks=True) + isis[process]) return None def apply(isis): # Save original configuration prior to starting any commit actions frr_cfg = {} frr_cfg['original_config'] = frr.get_configuration(daemon='isisd') frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], isis['new_frr_config'], from_re='router isis .*') # Debugging print('') print('--------- DEBUGGING ----------') print(f'Existing config:\n{frr_cfg["original_config"]}\n\n') print(f'Replacement config:\n{isis["new_frr_config"]}\n\n') print(f'Modified config:\n{frr_cfg["modified_config"]}\n\n') # FRR mark configuration will test for syntax errors and throws an # exception if any syntax errors is detected frr.mark_configuration(frr_cfg['modified_config']) # Commit resulting configuration to FRR, this will throw CommitError # on failure frr.reload_configuration(frr_cfg['modified_config'], daemon='isisd') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/protocols_mpls.py b/src/conf_mode/protocols_mpls.py index da298325c..791b18110 100755 --- a/src/conf_mode/protocols_mpls.py +++ b/src/conf_mode/protocols_mpls.py @@ -1,149 +1,148 @@ #!/usr/bin/env python3 # # Copyright (C) 2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import node_changed from vyos.template import render_to_string from vyos.util import call from vyos.util import dict_search from vyos import ConfigError from vyos import frr from vyos import airbag airbag.enable() config_file = r'/tmp/ldpd.frr' def get_config(config=None): if config: conf = config else: conf = Config() base = ['protocols', 'mpls'] mpls = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return mpls def verify(mpls): # If no config, then just bail out early. if not mpls: return None # Checks to see if LDP is properly configured if 'ldp' in mpls: # If router ID not defined if 'router_id' not in mpls['ldp']: raise ConfigError('Router ID missing. An LDP router id is mandatory!') # If interface not set if 'interface' not in mpls['ldp']: raise ConfigError('LDP interfaces are missing. An LDP interface is mandatory!') # If transport addresses are not set if not dict_search('ldp.discovery.transport_ipv4_address', mpls) and \ not dict_search('ldp.discovery.transport_ipv6_address', mpls): raise ConfigError('LDP transport address missing!') return None def generate(mpls): # If there's no MPLS config generated, create dictionary key with no value. if not mpls: mpls['new_frr_config'] = '' return None - mpls['new_frr_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls, - trim_blocks=True) + mpls['new_frr_config'] = render_to_string('frr/ldpd.frr.tmpl', mpls) return None def apply(mpls): # Define dictionary that will load FRR config frr_cfg = {} # Save original configuration prior to starting any commit actions frr_cfg['original_config'] = frr.get_configuration(daemon='ldpd') frr_cfg['modified_config'] = frr.replace_section(frr_cfg['original_config'], mpls['new_frr_config'], from_re='mpls.*') # If FRR config is blank, rerun the blank commit three times due to frr-reload # behavior/bug not properly clearing out on one commit. if mpls['new_frr_config'] == '': for x in range(3): frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') elif not 'ldp' in mpls: for x in range(3): frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') else: # FRR mark configuration will test for syntax errors and throws an # exception if any syntax errors is detected frr.mark_configuration(frr_cfg['modified_config']) # Commit resulting configuration to FRR, this will throw CommitError # on failure frr.reload_configuration(frr_cfg['modified_config'], daemon='ldpd') # Set number of entries in the platform label tables labels = '0' if 'interface' in mpls: labels = '1048575' call(f'sysctl -wq net.mpls.platform_labels={labels}') # Check for changes in global MPLS options if 'parameters' in mpls: # Choose whether to copy IP TTL to MPLS header TTL if 'no_propagate_ttl' in mpls['parameters']: call('sysctl -wq net.mpls.ip_ttl_propagate=0') # Choose whether to limit maximum MPLS header TTL if 'maximum_ttl' in mpls['parameters']: ttl = mpls['parameters']['maximum_ttl'] call(f'sysctl -wq net.mpls.default_ttl={ttl}') else: # Set default global MPLS options if not defined. call('sysctl -wq net.mpls.ip_ttl_propagate=1') call('sysctl -wq net.mpls.default_ttl=255') # Enable and disable MPLS processing on interfaces per configuration if 'interface' in mpls: system_interfaces = [] system_interfaces.append(((os.popen('sysctl net.mpls.conf').read()).split('\n'))) del system_interfaces[0][-1] for configured_interface in mpls['interface']: for system_interface in system_interfaces[0]: if configured_interface in system_interface: call(f'sysctl -wq net.mpls.conf.{configured_interface}.input=1') elif system_interface.endswith(' = 1'): system_interface = system_interface.replace(' = 1', '=0') call(f'sysctl -wq {system_interface}') else: # If MPLS interfaces are not configured, set MPLS processing disabled system_interfaces = [] system_interfaces.append(((os.popen('sysctl net.mpls.conf').read()).replace(" = 1", "=0")).split('\n')) del system_interfaces[0][-1] for interface in (system_interfaces[0]): call(f'sysctl -wq {interface}') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py index 27d0ee60c..67edeb630 100755 --- a/src/conf_mode/service_ids_fastnetmon.py +++ b/src/conf_mode/service_ids_fastnetmon.py @@ -1,92 +1,92 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos import ConfigError from vyos.util import call from vyos.template import render from vyos import airbag airbag.enable() config_file = r'/etc/fastnetmon.conf' networks_list = r'/etc/networks_list' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'ids', 'ddos-protection'] fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) return fastnetmon def verify(fastnetmon): if not fastnetmon: return None if not "mode" in fastnetmon: raise ConfigError('ddos-protection mode is mandatory!') if not "network" in fastnetmon: raise ConfigError('Required define network!') if not "listen_interface" in fastnetmon: raise ConfigError('Define listen-interface is mandatory!') if "alert_script" in fastnetmon: if os.path.isfile(fastnetmon["alert_script"]): # Check script permissions if not os.access(fastnetmon["alert_script"], os.X_OK): raise ConfigError('Script {0} does not have permissions for execution'.format(fastnetmon["alert_script"])) else: - raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"])) + raise ConfigError('File {0} does not exists!'.format(fastnetmon["alert_script"])) def generate(fastnetmon): if not fastnetmon: if os.path.isfile(config_file): os.unlink(config_file) if os.path.isfile(networks_list): os.unlink(networks_list) return - render(config_file, 'ids/fastnetmon.tmpl', fastnetmon, trim_blocks=True) - render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon, trim_blocks=True) + render(config_file, 'ids/fastnetmon.tmpl', fastnetmon) + render(networks_list, 'ids/fastnetmon_networks_list.tmpl', fastnetmon) return None def apply(fastnetmon): if not fastnetmon: # Stop fastnetmon service if removed call('systemctl stop fastnetmon.service') else: call('systemctl restart fastnetmon.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 68c554360..f676fdbbe 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -1,318 +1,318 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import re from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.util import call, get_half_cpus from vyos import ConfigError from vyos import airbag airbag.enable() ipoe_conf = '/run/accel-pppd/ipoe.conf' ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' default_config_data = { 'auth_mode': 'local', 'auth_interfaces': [], 'chap_secrets_file': ipoe_chap_secrets, # used in Jinja2 template 'interfaces': [], 'dnsv4': [], 'dnsv6': [], 'client_ipv6_pool': [], 'client_ipv6_delegate_prefix': [], 'radius_server': [], 'radius_acct_inter_jitter': '', 'radius_acct_tmo': '3', 'radius_max_try': '3', 'radius_timeout': '3', 'radius_nas_id': '', 'radius_nas_ip': '', 'radius_source_address': '', 'radius_shaper_attr': '', 'radius_shaper_vendor': '', 'radius_dynamic_author': '', 'thread_cnt': get_half_cpus() } def get_config(config=None): if config: conf = config else: conf = Config() base_path = ['service', 'ipoe-server'] if not conf.exists(base_path): return None conf.set_level(base_path) ipoe = deepcopy(default_config_data) for interface in conf.list_nodes(['interface']): tmp = { 'mode': 'L2', 'name': interface, 'shared': '1', # may need a config option, can be dhcpv4 or up for unclassified pkts 'sess_start': 'dhcpv4', 'range': None, 'ifcfg': '1', 'vlan_mon': [] } conf.set_level(base_path + ['interface', interface]) if conf.exists(['network-mode']): tmp['mode'] = conf.return_value(['network-mode']) if conf.exists(['network']): mode = conf.return_value(['network']) if mode == 'vlan': tmp['shared'] = '0' if conf.exists(['vlan-id']): tmp['vlan_mon'] += conf.return_values(['vlan-id']) if conf.exists(['vlan-range']): tmp['vlan_mon'] += conf.return_values(['vlan-range']) if conf.exists(['client-subnet']): tmp['range'] = conf.return_value(['client-subnet']) ipoe['interfaces'].append(tmp) conf.set_level(base_path) if conf.exists(['name-server']): for name_server in conf.return_values(['name-server']): if is_ipv4(name_server): ipoe['dnsv4'].append(name_server) else: ipoe['dnsv6'].append(name_server) if conf.exists(['authentication', 'mode']): ipoe['auth_mode'] = conf.return_value(['authentication', 'mode']) if conf.exists(['authentication', 'interface']): for interface in conf.list_nodes(['authentication', 'interface']): tmp = { 'name': interface, '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', mac]) if conf.exists(['rate-limit', 'download']): client['rate_download'] = conf.return_value(['rate-limit', 'download']) if conf.exists(['rate-limit', 'upload']): client['rate_upload'] = conf.return_value(['rate-limit', 'upload']) if conf.exists(['vlan-id']): client['vlan'] = conf.return_value(['vlan-id']) 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']): for server in conf.list_nodes(['authentication', 'radius', 'server']): radius = { 'server' : server, 'key' : '', 'fail_time' : 0, 'port' : '1812', 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['fail-time']): radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) if conf.exists(['acct-port']): radius['acct_port'] = conf.return_value(['acct-port']) if conf.exists(['key']): radius['key'] = conf.return_value(['key']) if not conf.exists(['disable']): ipoe['radius_server'].append(radius) # # advanced radius-setting conf.set_level(base_path + ['authentication', 'radius']) if conf.exists(['acct-interim-jitter']): ipoe['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) if conf.exists(['acct-timeout']): ipoe['radius_acct_tmo'] = conf.return_value(['acct-timeout']) if conf.exists(['max-try']): ipoe['radius_max_try'] = conf.return_value(['max-try']) if conf.exists(['timeout']): ipoe['radius_timeout'] = conf.return_value(['timeout']) if conf.exists(['nas-identifier']): ipoe['radius_nas_id'] = conf.return_value(['nas-identifier']) if conf.exists(['nas-ip-address']): ipoe['radius_nas_ip'] = conf.return_value(['nas-ip-address']) if conf.exists(['source-address']): ipoe['radius_source_address'] = conf.return_value(['source-address']) # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) if conf.exists(['dynamic-author']): dae = { 'port' : '', 'server' : '', 'key' : '' } if conf.exists(['dynamic-author', 'server']): dae['server'] = conf.return_value(['dynamic-author', 'server']) if conf.exists(['dynamic-author', 'port']): dae['port'] = conf.return_value(['dynamic-author', 'port']) if conf.exists(['dynamic-author', 'key']): dae['key'] = conf.return_value(['dynamic-author', 'key']) ipoe['radius_dynamic_author'] = dae conf.set_level(base_path) if conf.exists(['client-ipv6-pool', 'prefix']): for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): tmp = { 'prefix': prefix, 'mask': '64' } if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) ipoe['client_ipv6_pool'].append(tmp) if conf.exists(['client-ipv6-pool', 'delegate']): for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): tmp = { 'prefix': prefix, 'mask': '' } if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) ipoe['client_ipv6_delegate_prefix'].append(tmp) return ipoe def verify(ipoe): if not ipoe: return None if not ipoe['interfaces']: raise ConfigError('No IPoE interface configured') for interface in ipoe['interfaces']: if not interface['range']: raise ConfigError(f'No IPoE client subnet defined on interface "{ interface }"') if len(ipoe['dnsv4']) > 2: raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') if len(ipoe['dnsv6']) > 3: raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') if ipoe['auth_mode'] == 'radius': if len(ipoe['radius_server']) == 0: raise ConfigError('RADIUS authentication requires at least one server') for radius in ipoe['radius_server']: if not radius['key']: server = radius['server'] raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') if ipoe['client_ipv6_delegate_prefix'] and not ipoe['client_ipv6_pool']: raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') return None def generate(ipoe): if not ipoe: return None - render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe, trim_blocks=True) + render(ipoe_conf, 'accel-ppp/ipoe.config.tmpl', ipoe) if ipoe['auth_mode'] == 'local': render(ipoe_chap_secrets, 'accel-ppp/chap-secrets.ipoe.tmpl', ipoe) os.chmod(ipoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: if os.path.exists(ipoe_chap_secrets): os.unlink(ipoe_chap_secrets) return None def apply(ipoe): if ipoe == None: call('systemctl stop accel-ppp@ipoe.service') for file in [ipoe_conf, ipoe_chap_secrets]: if os.path.exists(file): os.unlink(file) return None call('systemctl restart accel-ppp@ipoe.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 2260b3fe1..9fbd531da 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -1,107 +1,107 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configverify import verify_accel_ppp_base_service from vyos.template import render from vyos.util import call from vyos.util import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' pppoe_chap_secrets = r'/run/accel-pppd/pppoe.chap-secrets' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'pppoe-server'] if not conf.exists(base): return None # retrieve common dictionary keys pppoe = get_accel_dict(conf, base, pppoe_chap_secrets) return pppoe def verify(pppoe): if not pppoe: return None verify_accel_ppp_base_service(pppoe) if 'wins_server' in pppoe and len(pppoe['wins_server']) > 2: raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') if 'interface' not in pppoe: raise ConfigError('At least one listen interface must be defined!') # local ippool and gateway settings config checks if not (dict_search('client_ip_pool.subnet', pppoe) or (dict_search('client_ip_pool.start', pppoe) and dict_search('client_ip_pool.stop', pppoe))): print('Warning: No PPPoE client pool defined') if dict_search('authentication.radius.dynamic_author.server', pppoe): if not dict_search('authentication.radius.dynamic_author.key', pppoe): raise ConfigError('DA/CoE server key required!') return None def generate(pppoe): if not pppoe: return None - render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True) + render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe) if dict_search('authentication.mode', pppoe) == 'local': render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', - pppoe, trim_blocks=True, permission=0o640) + pppoe, permission=0o640) else: if os.path.exists(pppoe_chap_secrets): os.unlink(pppoe_chap_secrets) return None def apply(pppoe): if not pppoe: call('systemctl stop accel-ppp@pppoe.service') for file in [pppoe_conf, pppoe_chap_secrets]: if os.path.exists(file): os.unlink(file) return None call('systemctl restart accel-ppp@pppoe.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 687d7068f..65eb11ce3 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -1,120 +1,120 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2019 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import call from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/run/radvd/radvd.conf' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'router-advert'] rtradv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_interface_values = defaults(base + ['interface']) # we deal with prefix defaults later on if 'prefix' in default_interface_values: del default_interface_values['prefix'] default_prefix_values = defaults(base + ['interface', 'prefix']) if 'interface' in rtradv: for interface in rtradv['interface']: rtradv['interface'][interface] = dict_merge( default_interface_values, rtradv['interface'][interface]) if 'prefix' in rtradv['interface'][interface]: for prefix in rtradv['interface'][interface]['prefix']: rtradv['interface'][interface]['prefix'][prefix] = dict_merge( default_prefix_values, rtradv['interface'][interface]['prefix'][prefix]) if 'name_server' in rtradv['interface'][interface]: # always use a list when dealing with nameservers - eases the template generation if isinstance(rtradv['interface'][interface]['name_server'], str): rtradv['interface'][interface]['name_server'] = [ rtradv['interface'][interface]['name_server']] return rtradv def verify(rtradv): if not rtradv: return None if 'interface' not in rtradv: return None for interface in rtradv['interface']: interface = rtradv['interface'][interface] if 'prefix' in interface: for prefix in interface['prefix']: prefix = interface['prefix'][prefix] valid_lifetime = prefix['valid_lifetime'] if valid_lifetime == 'infinity': valid_lifetime = 4294967295 preferred_lifetime = prefix['preferred_lifetime'] if preferred_lifetime == 'infinity': preferred_lifetime = 4294967295 if not (int(valid_lifetime) > int(preferred_lifetime)): raise ConfigError('Prefix valid-lifetime must be greater then preferred-lifetime') return None def generate(rtradv): if not rtradv: return None - render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, trim_blocks=True, permission=0o644) + render(config_file, 'router-advert/radvd.conf.tmpl', rtradv, permission=0o644) return None def apply(rtradv): if not rtradv: # bail out early - looks like removal from running config call('systemctl stop radvd.service') if os.path.exists(config_file): os.unlink(config_file) return None call('systemctl restart radvd.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/ssh.py b/src/conf_mode/ssh.py index e07745963..8f99053d2 100755 --- a/src/conf_mode/ssh.py +++ b/src/conf_mode/ssh.py @@ -1,95 +1,95 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.configverify import verify_vrf from vyos.util import call from vyos.template import render from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/run/ssh/sshd_config' systemd_override = r'/etc/systemd/system/ssh.service.d/override.conf' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'ssh'] if not conf.exists(base): return None ssh = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) ssh = dict_merge(default_values, ssh) # pass config file path - used in override template ssh['config_file'] = config_file return ssh def verify(ssh): if not ssh: return None verify_vrf(ssh) return None def generate(ssh): if not ssh: if os.path.isfile(config_file): os.unlink(config_file) if os.path.isfile(systemd_override): os.unlink(systemd_override) return None - render(config_file, 'ssh/sshd_config.tmpl', ssh, trim_blocks=True) - render(systemd_override, 'ssh/override.conf.tmpl', ssh, trim_blocks=True) + render(config_file, 'ssh/sshd_config.tmpl', ssh) + render(systemd_override, 'ssh/override.conf.tmpl', ssh) return None def apply(ssh): if not ssh: # SSH access is removed in the commit call('systemctl stop ssh.service') # Reload systemd manager configuration call('systemctl daemon-reload') if ssh: call('systemctl restart ssh.service') return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index 2c0bbd4f7..39bad717d 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -1,406 +1,406 @@ #!/usr/bin/env python3 # # Copyright (C) 2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from crypt import crypt, METHOD_SHA512 from netifaces import interfaces from psutil import users from pwd import getpwall, getpwnam from spwd import getspnam from sys import exit from vyos.config import Config from vyos.template import render from vyos.util import cmd, call, DEVNULL, chmod_600, chmod_755 from vyos import ConfigError from vyos import airbag airbag.enable() radius_config_file = "/etc/pam_radius_auth.conf" default_config_data = { 'deleted': False, 'add_users': [], 'del_users': [], 'radius_server': [], 'radius_source_address': '', 'radius_vrf': '' } def get_local_users(): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] for p in getpwall(): username = p[0] uid = getpwnam(username).pw_uid if uid in range(1000, 29999): if username not in ['radius_user', 'radius_priv_user']: local_users.append(username) return local_users def get_config(config=None): login = default_config_data if config: conf = config else: conf = Config() base_level = ['system', 'login'] # We do not need to check if the nodes exist or not and bail out early # ... this would interrupt the following logic on determine which users # should be deleted and which users should stay. # # All fine so far! # Read in all local users and store to list for username in conf.list_nodes(base_level + ['user']): user = { 'name': username, 'password_plaintext': '', 'password_encrypted': '!', 'public_keys': [], 'full_name': '', 'home_dir': '/home/' + username, } conf.set_level(base_level + ['user', username]) # Plaintext password if conf.exists(['authentication', 'plaintext-password']): user['password_plaintext'] = conf.return_value( ['authentication', 'plaintext-password']) # Encrypted password if conf.exists(['authentication', 'encrypted-password']): user['password_encrypted'] = conf.return_value( ['authentication', 'encrypted-password']) # User real name if conf.exists(['full-name']): user['full_name'] = conf.return_value(['full-name']) # User home-directory if conf.exists(['home-directory']): user['home_dir'] = conf.return_value(['home-directory']) # Read in public keys for id in conf.list_nodes(['authentication', 'public-keys']): key = { 'name': id, 'key': '', 'options': '', 'type': '' } conf.set_level(base_level + ['user', username, 'authentication', 'public-keys', id]) # Public Key portion if conf.exists(['key']): key['key'] = conf.return_value(['key']) # Options for individual public key if conf.exists(['options']): key['options'] = conf.return_value(['options']) # Type of public key if conf.exists(['type']): key['type'] = conf.return_value(['type']) # Append individual public key to list of user keys user['public_keys'].append(key) login['add_users'].append(user) # # RADIUS configuration # conf.set_level(base_level + ['radius']) 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 = { 'address': server, 'disabled': False, 'key': '', 'port': '1812', 'timeout': '2', 'priority': 255 } conf.set_level(base_level + ['radius', 'server', server]) # Check if RADIUS server was temporary disabled if conf.exists(['disable']): server_cfg['disabled'] = True # RADIUS shared secret if conf.exists(['key']): server_cfg['key'] = conf.return_value(['key']) # RADIUS authentication port if conf.exists(['port']): server_cfg['port'] = conf.return_value(['port']) # RADIUS session timeout if conf.exists(['timeout']): server_cfg['timeout'] = conf.return_value(['timeout']) # Check if RADIUS server has priority if conf.exists(['priority']): server_cfg['priority'] = int(conf.return_value(['priority'])) # Append individual RADIUS server configuration to global server list login['radius_server'].append(server_cfg) # users no longer existing in the running configuration need to be deleted local_users = get_local_users() cli_users = [tmp['name'] for tmp in login['add_users']] # create a list of all users, cli and users all_users = list(set(local_users+cli_users)) # Remove any normal users that dos not exist in the current configuration. # This can happen if user is added but configuration was not saved and # system is rebooted. login['del_users'] = [tmp for tmp in all_users if tmp not in cli_users] return login def verify(login): cur_user = os.environ['SUDO_USER'] if cur_user in login['del_users']: raise ConfigError( 'Attempting to delete current user: {}'.format(cur_user)) for user in login['add_users']: for key in user['public_keys']: if not key['type']: raise ConfigError( 'SSH public key type missing for "{name}"!'.format(**key)) if not key['key']: raise ConfigError( 'SSH public key for id "{name}" missing!'.format(**key)) # At lease one RADIUS server must not be disabled if len(login['radius_server']) > 0: fail = True for server in login['radius_server']: if not server['disabled']: fail = False 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 def generate(login): # calculate users encrypted password for user in login['add_users']: if user['password_plaintext']: user['password_encrypted'] = crypt( user['password_plaintext'], METHOD_SHA512) user['password_plaintext'] = '' # remove old plaintext password and set new encrypted password env = os.environ.copy() env['vyos_libexec_dir'] = '/usr/libexec/vyos' call("/opt/vyatta/sbin/my_delete system login user '{name}' " "authentication plaintext-password" .format(**user), env=env) call("/opt/vyatta/sbin/my_set system login user '{name}' " "authentication encrypted-password '{password_encrypted}'" .format(**user), env=env) else: try: if getspnam(user['name']).sp_pwdp == user['password_encrypted']: # If the current encrypted bassword matches the encrypted password # from the config - do not update it. This will remove the encrypted # value from the system logs. # # The encrypted password will be set only once during the first boot # after an image upgrade. user['password_encrypted'] = '' except: pass if len(login['radius_server']) > 0: render(radius_config_file, 'system-login/pam_radius_auth.conf.tmpl', - login, trim_blocks=True) + login) uid = getpwnam('root').pw_uid gid = getpwnam('root').pw_gid os.chown(radius_config_file, uid, gid) chmod_600(radius_config_file) else: if os.path.isfile(radius_config_file): os.unlink(radius_config_file) return None def apply(login): for user in login['add_users']: # make new user using vyatta shell and make home directory (-m), # default group of 100 (users) command = "useradd -m -N" # check if user already exists: if user['name'] in get_local_users(): # update existing account command = "usermod" # all accounts use /bin/vbash command += " -s /bin/vbash" # we need to use '' quotes when passing formatted data to the shell # else it will not work as some data parts are lost in translation if user['password_encrypted']: command += " -p '{}'".format(user['password_encrypted']) if user['full_name']: command += " -c '{}'".format(user['full_name']) if user['home_dir']: command += " -d '{}'".format(user['home_dir']) command += " -G frrvty,vyattacfg,sudo,adm,dip,disk" command += " {}".format(user['name']) try: cmd(command) uid = getpwnam(user['name']).pw_uid gid = getpwnam(user['name']).pw_gid # we should not rely on the value stored in user['home_dir'], as a # crazy user will choose username root or any other system user # which will fail. Should we deny using root at all? home_dir = getpwnam(user['name']).pw_dir # install ssh keys ssh_key_dir = home_dir + '/.ssh' if not os.path.isdir(ssh_key_dir): os.mkdir(ssh_key_dir) os.chown(ssh_key_dir, uid, gid) chmod_755(ssh_key_dir) ssh_key_file = ssh_key_dir + '/authorized_keys' with open(ssh_key_file, 'w') as f: f.write("# Automatically generated by VyOS\n") f.write("# Do not edit, all changes will be lost\n") for id in user['public_keys']: line = '' if id['options']: line = '{} '.format(id['options']) line += '{} {} {}\n'.format(id['type'], id['key'], id['name']) f.write(line) os.chown(ssh_key_file, uid, gid) chmod_600(ssh_key_file) except Exception as e: print(e) raise ConfigError('Adding user "{name}" raised exception' .format(**user)) for user in login['del_users']: try: # Logout user if he is logged in if user in list(set([tmp[0] for tmp in users()])): print('{} is logged in, forcing logout'.format(user)) call('pkill -HUP -u {}'.format(user)) # Remove user account but leave home directory to be safe call(f'userdel -r {user}', stderr=DEVNULL) except Exception as e: raise ConfigError(f'Deleting user "{user}" raised exception: {e}') # # RADIUS configuration # if len(login['radius_server']) > 0: try: env = os.environ.copy() env['DEBIAN_FRONTEND'] = 'noninteractive' # Enable RADIUS in PAM cmd("pam-auth-update --package --enable radius", env=env) # Make NSS system aware of RADIUS, too command = "sed -i -e \'/\smapname/b\' \ -e \'/^passwd:/s/\s\s*/&mapuid /\' \ -e \'/^passwd:.*#/s/#.*/mapname &/\' \ -e \'/^passwd:[^#]*$/s/$/ mapname &/\' \ -e \'/^group:.*#/s/#.*/ mapname &/\' \ -e \'/^group:[^#]*$/s/: */&mapname /\' \ /etc/nsswitch.conf" cmd(command) except Exception as e: raise ConfigError('RADIUS configuration failed: {}'.format(e)) else: try: env = os.environ.copy() env['DEBIAN_FRONTEND'] = 'noninteractive' # Disable RADIUS in PAM cmd("pam-auth-update --package --remove radius", env=env) command = "sed -i -e \'/^passwd:.*mapuid[ \t]/s/mapuid[ \t]//\' \ -e \'/^passwd:.*[ \t]mapname/s/[ \t]mapname//\' \ -e \'/^group:.*[ \t]mapname/s/[ \t]mapname//\' \ -e \'s/[ \t]*$//\' \ /etc/nsswitch.conf" cmd(command) except Exception as e: raise ConfigError( 'Removing RADIUS configuration failed.\n{}'.format(e)) return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 2376e5d44..447c97a78 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -1,136 +1,136 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from netifaces import interfaces from sys import exit from time import sleep from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import cmd from vyos.validate import is_addr_assigned from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() curlrc_config = r'/etc/curlrc' ssh_config = r'/etc/ssh/ssh_config' systemd_action_file = '/lib/systemd/system/ctrl-alt-del.target' def get_config(config=None): if config: conf = config else: conf = Config() base = ['system', 'option'] options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) options = dict_merge(default_values, options) return options def verify(options): if 'http_client' in options: config = options['http_client'] if 'source_interface' in config: if not config['source_interface'] in interfaces(): raise ConfigError(f'Source interface {source_interface} does not ' f'exist'.format(**config)) if {'source_address', 'source_interface'} <= set(config): raise ConfigError('Can not define both HTTP source-interface and source-address') if 'source_address' in config: if not is_addr_assigned(config['source_address']): raise ConfigError('No interface with give address specified!') if 'ssh_client' in options: config = options['ssh_client'] if 'source_address' in config: if not is_addr_assigned(config['source_address']): raise ConfigError('No interface with give address specified!') return None def generate(options): - render(curlrc_config, 'system/curlrc.tmpl', options, trim_blocks=True) - render(ssh_config, 'system/ssh_config.tmpl', options, trim_blocks=True) + render(curlrc_config, 'system/curlrc.tmpl', options) + render(ssh_config, 'system/ssh_config.tmpl', options) return None def apply(options): # System bootup beep if 'startup_beep' in options: cmd('systemctl enable vyos-beep.service') else: cmd('systemctl disable vyos-beep.service') # Ctrl-Alt-Delete action if os.path.exists(systemd_action_file): os.unlink(systemd_action_file) if 'ctrl_alt_del' in options: if options['ctrl_alt_del'] == 'reboot': os.symlink('/lib/systemd/system/reboot.target', systemd_action_file) elif options['ctrl_alt_del'] == 'poweroff': os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file) # Configure HTTP client if 'http_client' not in options: if os.path.exists(curlrc_config): os.unlink(curlrc_config) # Configure SSH client if 'ssh_client' not in options: if os.path.exists(ssh_config): os.unlink(ssh_config) # Reboot system on kernel panic with open('/proc/sys/kernel/panic', 'w') as f: if 'reboot_on_panic' in options: f.write('60') else: f.write('0') # tuned - performance tuning if 'performance' in options: cmd('systemctl restart tuned.service') # wait until daemon has started before sending configuration while (int(os.system('systemctl is-active --quiet tuned.service')) != 0): sleep(0.250) cmd('tuned-adm profile network-{performance}'.format(**options)) else: cmd('systemctl stop tuned.service') # Keyboard layout - there will be always the default key inside the dict # but we check for key existence anyway if 'keyboard_layout' in options: cmd('loadkeys -C /dev/console {keyboard_layout}'.format(**options)) if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py index b1daf7a82..3d8a51cd8 100755 --- a/src/conf_mode/system-syslog.py +++ b/src/conf_mode/system-syslog.py @@ -1,268 +1,268 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import re from sys import exit from vyos.config import Config from vyos import ConfigError from vyos.util import run from vyos.template import render from vyos import airbag airbag.enable() def get_config(config=None): if config: c = config else: c = Config() if not c.exists('system syslog'): return None c.set_level('system syslog') config_data = { 'files': {}, 'console': {}, 'hosts': {}, 'user': {} } # # /etc/rsyslog.d/vyos-rsyslog.conf # 'set system syslog global' # config_data['files'].update( { 'global': { 'log-file': '/var/log/messages', 'max-size': 262144, 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog', 'selectors': '*.notice;local7.debug', 'max-files': '5', 'preserver_fqdn': False } } ) if c.exists('global marker'): config_data['files']['global']['marker'] = True if c.exists('global marker interval'): config_data['files']['global'][ 'marker-interval'] = c.return_value('global marker interval') if c.exists('global facility'): config_data['files']['global'][ 'selectors'] = generate_selectors(c, 'global facility') if c.exists('global archive size'): config_data['files']['global']['max-size'] = int( c.return_value('global archive size')) * 1024 if c.exists('global archive file'): config_data['files']['global'][ 'max-files'] = c.return_value('global archive file') if c.exists('global preserve-fqdn'): config_data['files']['global']['preserver_fqdn'] = True # # set system syslog file # if c.exists('file'): filenames = c.list_nodes('file') for filename in filenames: config_data['files'].update( { filename: { 'log-file': '/var/log/user/' + filename, 'max-files': '5', 'action-on-max-size': '/usr/sbin/logrotate /etc/logrotate.d/' + filename, 'selectors': '*.err', 'max-size': 262144 } } ) if c.exists('file ' + filename + ' facility'): config_data['files'][filename]['selectors'] = generate_selectors( c, 'file ' + filename + ' facility') if c.exists('file ' + filename + ' archive size'): config_data['files'][filename]['max-size'] = int( c.return_value('file ' + filename + ' archive size')) * 1024 if c.exists('file ' + filename + ' archive files'): config_data['files'][filename]['max-files'] = c.return_value( 'file ' + filename + ' archive files') # set system syslog console if c.exists('console'): config_data['console'] = { '/dev/console': { 'selectors': '*.err' } } for f in c.list_nodes('console facility'): if c.exists('console facility ' + f + ' level'): config_data['console'] = { '/dev/console': { 'selectors': generate_selectors(c, 'console facility') } } # set system syslog host if c.exists('host'): rhosts = c.list_nodes('host') proto = 'udp' for rhost in rhosts: for fac in c.list_nodes('host ' + rhost + ' facility'): if c.exists('host ' + rhost + ' facility ' + fac + ' protocol'): proto = c.return_value( 'host ' + rhost + ' facility ' + fac + ' protocol') else: proto = 'udp' config_data['hosts'].update( { rhost: { 'selectors': generate_selectors(c, 'host ' + rhost + ' facility'), 'proto': proto } } ) if c.exists('host ' + rhost + ' port'): config_data['hosts'][rhost][ 'port'] = c.return_value(['host', rhost, 'port']) # set system syslog host x.x.x.x format octet-counted if c.exists('host ' + rhost + ' format octet-counted'): config_data['hosts'][rhost]['oct_count'] = True else: config_data['hosts'][rhost]['oct_count'] = False # set system syslog user if c.exists('user'): usrs = c.list_nodes('user') for usr in usrs: config_data['user'].update( { usr: { 'selectors': generate_selectors(c, 'user ' + usr + ' facility') } } ) return config_data def generate_selectors(c, config_node): # protocols and security are being mapped here # for backward compatibility with old configs # security and protocol mappings can be removed later nodes = c.list_nodes(config_node) selectors = "" for node in nodes: lvl = c.return_value(config_node + ' ' + node + ' level') if lvl == None: lvl = "err" if lvl == 'all': lvl = '*' if node == 'all' and node != nodes[-1]: selectors += "*." + lvl + ";" elif node == 'all': selectors += "*." + lvl elif node != nodes[-1]: if node == 'protocols': node = 'local7' if node == 'security': node = 'auth' selectors += node + "." + lvl + ";" else: if node == 'protocols': node = 'local7' if node == 'security': node = 'auth' selectors += node + "." + lvl return selectors def generate(c): if c == None: return None conf = '/etc/rsyslog.d/vyos-rsyslog.conf' - render(conf, 'syslog/rsyslog.conf.tmpl', c, trim_blocks=True) + render(conf, 'syslog/rsyslog.conf.tmpl', c) # eventually write for each file its own logrotate file, since size is # defined it shouldn't matter conf = '/etc/logrotate.d/vyos-rsyslog' - render(conf, 'syslog/logrotate.tmpl', c, trim_blocks=True) + render(conf, 'syslog/logrotate.tmpl', c) def verify(c): if c == None: return None # may be obsolete # /etc/rsyslog.conf is generated somewhere and copied over the original (exists in /opt/vyatta/etc/rsyslog.conf) # it interferes with the global logging, to make sure we are using a single base, template is enforced here # if not os.path.islink('/etc/rsyslog.conf'): os.remove('/etc/rsyslog.conf') os.symlink( '/usr/share/vyos/templates/rsyslog/rsyslog.conf', '/etc/rsyslog.conf') # /var/log/vyos-rsyslog were the old files, we may want to clean those up, but currently there # is a chance that someone still needs it, so I don't automatically remove # them # if c == None: return None fac = [ '*', 'auth', 'authpriv', 'cron', 'daemon', 'kern', 'lpr', 'mail', 'mark', 'news', 'protocols', 'security', 'syslog', 'user', 'uucp', 'local0', 'local1', 'local2', 'local3', 'local4', 'local5', 'local6', 'local7'] lvl = ['emerg', 'alert', 'crit', 'err', 'warning', 'notice', 'info', 'debug', '*'] for conf in c: if c[conf]: for item in c[conf]: for s in c[conf][item]['selectors'].split(";"): f = re.sub("\..*$", "", s) if f not in fac: raise ConfigError( 'Invalid facility ' + s + ' set in ' + conf + ' ' + item) l = re.sub("^.+\.", "", s) if l not in lvl: raise ConfigError( 'Invalid logging level ' + s + ' set in ' + conf + ' ' + item) def apply(c): if not c: return run('systemctl stop syslog.service') return run('systemctl restart syslog.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/system_lcd.py b/src/conf_mode/system_lcd.py index a540d1b9e..b5ce32beb 100755 --- a/src/conf_mode/system_lcd.py +++ b/src/conf_mode/system_lcd.py @@ -1,91 +1,91 @@ #!/usr/bin/env python3 # # Copyright 2020 VyOS maintainers and contributors <maintainers@vyos.io> # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.util import call from vyos.util import find_device_file from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() lcdd_conf = '/run/LCDd/LCDd.conf' lcdproc_conf = '/run/lcdproc/lcdproc.conf' def get_config(config=None): if config: conf = config else: conf = Config() base = ['system', 'lcd'] lcd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # Return (possibly empty) dictionary return lcd def verify(lcd): if not lcd: return None if 'model' in lcd and lcd['model'] in ['sdec']: # This is a fixed LCD display, no device needed - bail out early return None if not {'device', 'model'} <= set(lcd): raise ConfigError('Both device and driver must be set!') return None def generate(lcd): if not lcd: return None if 'device' in lcd: lcd['device'] = find_device_file(lcd['device']) # Render config file for daemon LCDd - render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd, trim_blocks=True) + render(lcdd_conf, 'lcd/LCDd.conf.tmpl', lcd) # Render config file for client lcdproc - render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd, trim_blocks=True) + render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd) return None def apply(lcd): if not lcd: call('systemctl stop lcdproc.service LCDd.service') for file in [lcdd_conf, lcdproc_conf]: if os.path.exists(file): os.remove(file) else: # Restart server call('systemctl restart LCDd.service lcdproc.service') return None if __name__ == '__main__': try: config_dict = get_config() verify(config_dict) generate(config_dict) apply(config_dict) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/tftp_server.py b/src/conf_mode/tftp_server.py index 56e195b6a..2409eec1f 100755 --- a/src/conf_mode/tftp_server.py +++ b/src/conf_mode/tftp_server.py @@ -1,140 +1,140 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import stat import pwd from copy import deepcopy from glob import glob from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.template import is_ipv4 from vyos.util import call from vyos.util import chmod_755 from vyos.validate import is_addr_assigned from vyos.xml import defaults from vyos import ConfigError from vyos import airbag airbag.enable() config_file = r'/etc/default/tftpd' def get_config(config=None): if config: conf = config else: conf = Config() base = ['service', 'tftp-server'] if not conf.exists(base): return None tftpd = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) # We have gathered the dict representation of the CLI, but there are default # options which we need to update into the dictionary retrived. default_values = defaults(base) tftpd = dict_merge(default_values, tftpd) return tftpd def verify(tftpd): # bail out early - looks like removal from running config if not tftpd: return None # Configuring allowed clients without a server makes no sense if 'directory' not in tftpd: raise ConfigError('TFTP root directory must be configured!') if 'listen_address' not in tftpd: raise ConfigError('TFTP server listen address must be configured!') for address in tftpd['listen_address']: if not is_addr_assigned(address): print(f'WARNING: TFTP server listen address "{address}" not ' \ 'assigned to any interface!') return None def generate(tftpd): # cleanup any available configuration file # files will be recreated on demand for i in glob(config_file + '*'): os.unlink(i) # bail out early - looks like removal from running config if tftpd is None: return None idx = 0 for address in tftpd['listen_address']: config = deepcopy(tftpd) port = tftpd['port'] if is_ipv4(address): config['listen_address'] = f'{address}:{port} -4' else: config['listen_address'] = f'[{address}]:{port} -6' file = config_file + str(idx) - render(file, 'tftp-server/default.tmpl', config, trim_blocks=True) + render(file, 'tftp-server/default.tmpl', config) idx = idx + 1 return None def apply(tftpd): # stop all services first - then we will decide call('systemctl stop tftpd@*.service') # bail out early - e.g. service deletion if tftpd is None: return None tftp_root = tftpd['directory'] if not os.path.exists(tftp_root): os.makedirs(tftp_root) chmod_755(tftp_root) # get UNIX uid for user 'tftp' tftp_uid = pwd.getpwnam('tftp').pw_uid tftp_gid = pwd.getpwnam('tftp').pw_gid # get UNIX uid for tftproot directory dir_uid = os.stat(tftp_root).st_uid dir_gid = os.stat(tftp_root).st_gid # adjust uid/gid of tftproot directory if files don't belong to user tftp if (tftp_uid != dir_uid) or (tftp_gid != dir_gid): os.chown(tftp_root, tftp_uid, tftp_gid) idx = 0 for address in tftpd['listen_address']: call(f'systemctl restart tftpd@{idx}.service') idx = idx + 1 return None if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 80eb8daf2..e970d2ef5 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -1,392 +1,392 @@ #!/usr/bin/env python3 # # Copyright (C) 2019-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import re from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from time import sleep from ipaddress import ip_network from vyos.config import Config from vyos.template import is_ipv4 from vyos.template import render from vyos.util import call, get_half_cpus from vyos import ConfigError from vyos import airbag airbag.enable() l2tp_conf = '/run/accel-pppd/l2tp.conf' l2tp_chap_secrets = '/run/accel-pppd/l2tp.chap-secrets' default_config_data = { 'auth_mode': 'local', 'auth_ppp_mppe': 'prefer', 'auth_proto': ['auth_mschap_v2'], 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template 'client_ip_pool': None, 'client_ip_subnets': [], 'client_ipv6_pool': [], 'client_ipv6_delegate_prefix': [], 'dnsv4': [], 'dnsv6': [], 'gateway_address': '10.255.255.0', 'local_users' : [], 'mtu': '1436', 'outside_addr': '', 'ppp_mppe': 'prefer', 'ppp_echo_failure' : '3', 'ppp_echo_interval' : '30', 'ppp_echo_timeout': '0', 'radius_server': [], 'radius_acct_inter_jitter': '', 'radius_acct_tmo': '3', 'radius_max_try': '3', 'radius_timeout': '3', 'radius_nas_id': '', 'radius_nas_ip': '', 'radius_source_address': '', 'radius_shaper_attr': '', 'radius_shaper_vendor': '', 'radius_dynamic_author': '', 'wins': [], 'ip6_column': [], 'thread_cnt': get_half_cpus() } def get_config(config=None): if config: conf = config else: conf = Config() base_path = ['vpn', 'l2tp', 'remote-access'] if not conf.exists(base_path): return None conf.set_level(base_path) l2tp = deepcopy(default_config_data) ### general options ### if conf.exists(['name-server']): for name_server in conf.return_values(['name-server']): if is_ipv4(name_server): l2tp['dnsv4'].append(name_server) else: l2tp['dnsv6'].append(name_server) if conf.exists(['wins-server']): l2tp['wins'] = conf.return_values(['wins-server']) if conf.exists('outside-address'): l2tp['outside_addr'] = conf.return_value('outside-address') if conf.exists(['authentication', 'mode']): l2tp['auth_mode'] = conf.return_value(['authentication', 'mode']) if conf.exists(['authentication', 'require']): l2tp['auth_proto'] = [] auth_mods = { 'pap': 'auth_pap', 'chap': 'auth_chap_md5', 'mschap': 'auth_mschap_v1', 'mschap-v2': 'auth_mschap_v2' } for proto in conf.return_values(['authentication', 'require']): l2tp['auth_proto'].append(auth_mods[proto]) if conf.exists(['authentication', 'mppe']): l2tp['auth_ppp_mppe'] = conf.return_value(['authentication', 'mppe']) # # local auth if conf.exists(['authentication', 'local-users']): for username in conf.list_nodes(['authentication', 'local-users', 'username']): user = { 'name' : username, 'password' : '', 'state' : 'enabled', 'ip' : '*', 'upload' : None, 'download' : None } conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) if conf.exists(['password']): user['password'] = conf.return_value(['password']) if conf.exists(['disable']): user['state'] = 'disable' if conf.exists(['static-ip']): user['ip'] = conf.return_value(['static-ip']) if conf.exists(['rate-limit', 'download']): user['download'] = conf.return_value(['rate-limit', 'download']) if conf.exists(['rate-limit', 'upload']): user['upload'] = conf.return_value(['rate-limit', 'upload']) l2tp['local_users'].append(user) # # RADIUS auth and settings conf.set_level(base_path + ['authentication', 'radius']) if conf.exists(['server']): for server in conf.list_nodes(['server']): radius = { 'server' : server, 'key' : '', 'fail_time' : 0, 'port' : '1812', 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['disable-accounting']): radius['acct_port'] = '0' if conf.exists(['fail-time']): radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) if conf.exists(['acct-port']): radius['acct_port'] = conf.return_value(['acct-port']) if conf.exists(['key']): radius['key'] = conf.return_value(['key']) if not conf.exists(['disable']): l2tp['radius_server'].append(radius) # # advanced radius-setting conf.set_level(base_path + ['authentication', 'radius']) if conf.exists(['acct-interim-jitter']): l2tp['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) if conf.exists(['acct-timeout']): l2tp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) if conf.exists(['max-try']): l2tp['radius_max_try'] = conf.return_value(['max-try']) if conf.exists(['timeout']): l2tp['radius_timeout'] = conf.return_value(['timeout']) if conf.exists(['nas-identifier']): l2tp['radius_nas_id'] = conf.return_value(['nas-identifier']) if conf.exists(['nas-ip-address']): l2tp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) if conf.exists(['source-address']): l2tp['radius_source_address'] = conf.return_value(['source-address']) # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) if conf.exists(['dynamic-author']): dae = { 'port' : '', 'server' : '', 'key' : '' } if conf.exists(['dynamic-author', 'server']): dae['server'] = conf.return_value(['dynamic-author', 'server']) if conf.exists(['dynamic-author', 'port']): dae['port'] = conf.return_value(['dynamic-author', 'port']) if conf.exists(['dynamic-author', 'key']): dae['key'] = conf.return_value(['dynamic-author', 'key']) l2tp['radius_dynamic_author'] = dae if conf.exists(['rate-limit', 'enable']): l2tp['radius_shaper_attr'] = 'Filter-Id' c_attr = ['rate-limit', 'enable', 'attribute'] if conf.exists(c_attr): l2tp['radius_shaper_attr'] = conf.return_value(c_attr) c_vendor = ['rate-limit', 'enable', 'vendor'] if conf.exists(c_vendor): l2tp['radius_shaper_vendor'] = conf.return_value(c_vendor) conf.set_level(base_path) if conf.exists(['client-ip-pool']): if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): start = conf.return_value(['client-ip-pool', 'start']) stop = conf.return_value(['client-ip-pool', 'stop']) l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) if conf.exists(['client-ip-pool', 'subnet']): l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) if conf.exists(['client-ipv6-pool', 'prefix']): l2tp['ip6_column'].append('ip6') for prefix in conf.list_nodes(['client-ipv6-pool', 'prefix']): tmp = { 'prefix': prefix, 'mask': '64' } if conf.exists(['client-ipv6-pool', 'prefix', prefix, 'mask']): tmp['mask'] = conf.return_value(['client-ipv6-pool', 'prefix', prefix, 'mask']) l2tp['client_ipv6_pool'].append(tmp) if conf.exists(['client-ipv6-pool', 'delegate']): l2tp['ip6_column'].append('ip6-db') for prefix in conf.list_nodes(['client-ipv6-pool', 'delegate']): tmp = { 'prefix': prefix, 'mask': '' } if conf.exists(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']): tmp['mask'] = conf.return_value(['client-ipv6-pool', 'delegate', prefix, 'delegation-prefix']) l2tp['client_ipv6_delegate_prefix'].append(tmp) if conf.exists(['mtu']): l2tp['mtu'] = conf.return_value(['mtu']) # gateway address if conf.exists(['gateway-address']): l2tp['gateway_address'] = conf.return_value(['gateway-address']) else: # calculate gw-ip-address if conf.exists(['client-ip-pool', 'start']): # use start ip as gw-ip-address l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) elif conf.exists(['client-ip-pool', 'subnet']): # use first ip address from first defined pool subnet = conf.return_values(['client-ip-pool', 'subnet'])[0] subnet = ip_network(subnet) l2tp['gateway_address'] = str(list(subnet.hosts())[0]) # LNS secret if conf.exists(['lns', 'shared-secret']): l2tp['lns_shared_secret'] = conf.return_value(['lns', 'shared-secret']) if conf.exists(['ccp-disable']): l2tp['ccp_disable'] = True # PPP options if conf.exists(['idle']): l2tp['ppp_echo_timeout'] = conf.return_value(['idle']) if conf.exists(['ppp-options', 'lcp-echo-failure']): l2tp['ppp_echo_failure'] = conf.return_value(['ppp-options', 'lcp-echo-failure']) if conf.exists(['ppp-options', 'lcp-echo-interval']): l2tp['ppp_echo_interval'] = conf.return_value(['ppp-options', 'lcp-echo-interval']) return l2tp def verify(l2tp): if not l2tp: return None if l2tp['auth_mode'] == 'local': if not l2tp['local_users']: raise ConfigError('L2TP local auth mode requires local users to be configured!') for user in l2tp['local_users']: if not user['password']: raise ConfigError(f"Password required for user {user['name']}") elif l2tp['auth_mode'] == 'radius': if len(l2tp['radius_server']) == 0: raise ConfigError("RADIUS authentication requires at least one server") for radius in l2tp['radius_server']: if not radius['key']: raise ConfigError(f"Missing RADIUS secret for server { radius['key'] }") # check for the existence of a client ip pool if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): raise ConfigError( "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") # check ipv6 if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']: raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix') for prefix in l2tp['client_ipv6_delegate_prefix']: if not prefix['mask']: raise ConfigError('Delegation-prefix required for individual delegated networks') if len(l2tp['wins']) > 2: raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') if len(l2tp['dnsv4']) > 2: raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') if len(l2tp['dnsv6']) > 3: raise ConfigError('Not more then three IPv6 DNS name-servers can be configured') return None def generate(l2tp): if not l2tp: return None - render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp, trim_blocks=True) + render(l2tp_conf, 'accel-ppp/l2tp.config.tmpl', l2tp) if l2tp['auth_mode'] == 'local': render(l2tp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', l2tp) os.chmod(l2tp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: if os.path.exists(l2tp_chap_secrets): os.unlink(l2tp_chap_secrets) return None def apply(l2tp): if not l2tp: call('systemctl stop accel-ppp@l2tp.service') for file in [l2tp_chap_secrets, l2tp_conf]: if os.path.exists(file): os.unlink(file) return None call('systemctl restart accel-ppp@l2tp.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index af8604972..b2aa13c0d 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -1,135 +1,134 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import dict_merge from vyos.xml import defaults from vyos.template import render from vyos.util import call from vyos import ConfigError from crypt import crypt, mksalt, METHOD_SHA512 from vyos import airbag airbag.enable() cfg_dir = '/run/ocserv' ocserv_conf = cfg_dir + '/ocserv.conf' ocserv_passwd = cfg_dir + '/ocpasswd' radius_cfg = cfg_dir + '/radiusclient.conf' radius_servers = cfg_dir + '/radius_servers' - # Generate hash from user cleartext password def get_hash(password): return crypt(password, mksalt(METHOD_SHA512)) - def get_config(): conf = Config() base = ['vpn', 'openconnect'] if not conf.exists(base): return None ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) - return ocserv + return ocserv def verify(ocserv): if ocserv is None: return None # Check authentication if "authentication" in ocserv: if "mode" in ocserv["authentication"]: if "local" in ocserv["authentication"]["mode"]: if not ocserv["authentication"]["local_users"] or not ocserv["authentication"]["local_users"]["username"]: raise ConfigError('openconnect mode local required at leat one user') else: for user in ocserv["authentication"]["local_users"]["username"]: if not "password" in ocserv["authentication"]["local_users"]["username"][user]: raise ConfigError(f'password required for user {user}') else: raise ConfigError('openconnect authentication mode required') else: raise ConfigError('openconnect authentication credentials required') # Check ssl if "ssl" in ocserv: req_cert = ['ca_cert_file', 'cert_file', 'key_file'] for cert in req_cert: if not cert in ocserv["ssl"]: raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-'))) else: raise ConfigError('openconnect ssl required') # Check network settings if "network_settings" in ocserv: if "push_route" in ocserv["network_settings"]: # Replace default route if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]: ocserv["network_settings"]["push_route"].remove("0.0.0.0/0") ocserv["network_settings"]["push_route"].append("default") else: - ocserv["network_settings"]["push_route"] = "default" + ocserv["network_settings"]["push_route"] = "default" else: raise ConfigError('openconnect network settings required') def generate(ocserv): if not ocserv: return None if "radius" in ocserv["authentication"]["mode"]: # Render radius client configuration - render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + render(radius_cfg, 'ocserv/radius_conf.tmpl', ocserv["authentication"]["radius"]) # Render radius servers - render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True) + render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"]) else: if "local_users" in ocserv["authentication"]: for user in ocserv["authentication"]["local_users"]["username"]: ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"]) # Render local users - render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"], trim_blocks=True) + render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) # Render config - render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True) - + render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv) def apply(ocserv): if not ocserv: call('systemctl stop ocserv.service') for file in [ocserv_conf, ocserv_passwd]: if os.path.exists(file): os.unlink(file) else: call('systemctl restart ocserv.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index 3125ee9d0..30abe4782 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -1,296 +1,296 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os import re from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit from vyos.config import Config from vyos.template import render from vyos.util import call, get_half_cpus from vyos import ConfigError from vyos import airbag airbag.enable() pptp_conf = '/run/accel-pppd/pptp.conf' pptp_chap_secrets = '/run/accel-pppd/pptp.chap-secrets' default_pptp = { 'auth_mode' : 'local', 'local_users' : [], 'radius_server' : [], 'radius_acct_inter_jitter': '', 'radius_acct_tmo' : '30', 'radius_max_try' : '3', 'radius_timeout' : '30', 'radius_nas_id' : '', 'radius_nas_ip' : '', 'radius_source_address' : '', 'radius_shaper_attr' : '', 'radius_shaper_vendor': '', 'radius_dynamic_author' : '', 'chap_secrets_file': pptp_chap_secrets, # used in Jinja2 template 'outside_addr': '', 'dnsv4': [], 'wins': [], 'client_ip_pool': '', 'mtu': '1436', 'auth_proto' : ['auth_mschap_v2'], 'ppp_mppe' : 'prefer', 'thread_cnt': get_half_cpus() } def get_config(config=None): if config: conf = config else: conf = Config() base_path = ['vpn', 'pptp', 'remote-access'] if not conf.exists(base_path): return None pptp = deepcopy(default_pptp) conf.set_level(base_path) if conf.exists(['name-server']): pptp['dnsv4'] = conf.return_values(['name-server']) if conf.exists(['wins-server']): pptp['wins'] = conf.return_values(['wins-server']) if conf.exists(['outside-address']): pptp['outside_addr'] = conf.return_value(['outside-address']) if conf.exists(['authentication', 'mode']): pptp['auth_mode'] = conf.return_value(['authentication', 'mode']) # # local auth if conf.exists(['authentication', 'local-users']): for username in conf.list_nodes(['authentication', 'local-users', 'username']): user = { 'name': username, 'password' : '', 'state' : 'enabled', 'ip' : '*', } conf.set_level(base_path + ['authentication', 'local-users', 'username', username]) if conf.exists(['password']): user['password'] = conf.return_value(['password']) if conf.exists(['disable']): user['state'] = 'disable' if conf.exists(['static-ip']): user['ip'] = conf.return_value(['static-ip']) if not conf.exists(['disable']): pptp['local_users'].append(user) # # RADIUS auth and settings conf.set_level(base_path + ['authentication', 'radius']) if conf.exists(['server']): for server in conf.list_nodes(['server']): radius = { 'server' : server, 'key' : '', 'fail_time' : 0, 'port' : '1812', 'acct_port' : '1813' } conf.set_level(base_path + ['authentication', 'radius', 'server', server]) if conf.exists(['disable-accounting']): radius['acct_port'] = '0' if conf.exists(['fail-time']): radius['fail_time'] = conf.return_value(['fail-time']) if conf.exists(['port']): radius['port'] = conf.return_value(['port']) if conf.exists(['acct-port']): radius['acct_port'] = conf.return_value(['acct-port']) if conf.exists(['key']): radius['key'] = conf.return_value(['key']) if not conf.exists(['disable']): pptp['radius_server'].append(radius) # # advanced radius-setting conf.set_level(base_path + ['authentication', 'radius']) if conf.exists(['acct-interim-jitter']): pptp['radius_acct_inter_jitter'] = conf.return_value(['acct-interim-jitter']) if conf.exists(['acct-timeout']): pptp['radius_acct_tmo'] = conf.return_value(['acct-timeout']) if conf.exists(['max-try']): pptp['radius_max_try'] = conf.return_value(['max-try']) if conf.exists(['timeout']): pptp['radius_timeout'] = conf.return_value(['timeout']) if conf.exists(['nas-identifier']): pptp['radius_nas_id'] = conf.return_value(['nas-identifier']) if conf.exists(['nas-ip-address']): pptp['radius_nas_ip'] = conf.return_value(['nas-ip-address']) if conf.exists(['source-address']): pptp['radius_source_address'] = conf.return_value(['source-address']) # Dynamic Authorization Extensions (DOA)/Change Of Authentication (COA) if conf.exists(['dae-server']): dae = { 'port' : '', 'server' : '', 'key' : '' } if conf.exists(['dynamic-author', 'ip-address']): dae['server'] = conf.return_value(['dynamic-author', 'ip-address']) if conf.exists(['dynamic-author', 'port']): dae['port'] = conf.return_value(['dynamic-author', 'port']) if conf.exists(['dynamic-author', 'key']): dae['key'] = conf.return_value(['dynamic-author', 'key']) pptp['radius_dynamic_author'] = dae if conf.exists(['rate-limit', 'enable']): pptp['radius_shaper_attr'] = 'Filter-Id' c_attr = ['rate-limit', 'enable', 'attribute'] if conf.exists(c_attr): pptp['radius_shaper_attr'] = conf.return_value(c_attr) c_vendor = ['rate-limit', 'enable', 'vendor'] if conf.exists(c_vendor): pptp['radius_shaper_vendor'] = conf.return_value(c_vendor) conf.set_level(base_path) if conf.exists(['client-ip-pool']): if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): start = conf.return_value(['client-ip-pool', 'start']) stop = conf.return_value(['client-ip-pool', 'stop']) pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) if conf.exists(['mtu']): pptp['mtu'] = conf.return_value(['mtu']) # gateway address if conf.exists(['gateway-address']): pptp['gw_ip'] = conf.return_value(['gateway-address']) else: # calculate gw-ip-address if conf.exists(['client-ip-pool', 'start']): # use start ip as gw-ip-address pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) if conf.exists(['authentication', 'require']): # clear default list content, now populate with actual CLI values pptp['auth_proto'] = [] auth_mods = { 'pap': 'auth_pap', 'chap': 'auth_chap_md5', 'mschap': 'auth_mschap_v1', 'mschap-v2': 'auth_mschap_v2' } for proto in conf.return_values(['authentication', 'require']): pptp['auth_proto'].append(auth_mods[proto]) if conf.exists(['authentication', 'mppe']): pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe']) return pptp def verify(pptp): if not pptp: return None if pptp['auth_mode'] == 'local': if not pptp['local_users']: raise ConfigError('PPTP local auth mode requires local users to be configured!') for user in pptp['local_users']: username = user['name'] if not user['password']: raise ConfigError(f'Password required for local user "{username}"') elif pptp['auth_mode'] == 'radius': if len(pptp['radius_server']) == 0: raise ConfigError('RADIUS authentication requires at least one server') for radius in pptp['radius_server']: if not radius['key']: server = radius['server'] raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') if len(pptp['dnsv4']) > 2: raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') if len(pptp['wins']) > 2: raise ConfigError('Not more then two IPv4 WINS name-servers can be configured') def generate(pptp): if not pptp: return None - render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp, trim_blocks=True) + render(pptp_conf, 'accel-ppp/pptp.config.tmpl', pptp) if pptp['local_users']: - render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True) + render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp) os.chmod(pptp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP) else: if os.path.exists(pptp_chap_secrets): os.unlink(pptp_chap_secrets) def apply(pptp): if not pptp: call('systemctl stop accel-ppp@pptp.service') for file in [pptp_conf, pptp_chap_secrets]: if os.path.exists(file): os.unlink(file) return None call('systemctl restart accel-ppp@pptp.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 1b2b80ce5..47367f125 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -1,116 +1,116 @@ #!/usr/bin/env python3 # # Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import os from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configverify import verify_accel_ppp_base_service from vyos.template import render from vyos.util import call from vyos.util import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() sstp_conf = '/run/accel-pppd/sstp.conf' sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' def get_config(config=None): if config: conf = config else: conf = Config() base = ['vpn', 'sstp'] if not conf.exists(base): return None # retrieve common dictionary keys sstp = get_accel_dict(conf, base, sstp_chap_secrets) return sstp def verify(sstp): if not sstp: return None verify_accel_ppp_base_service(sstp) if not sstp['client_ip_pool']: raise ConfigError('Client IP subnet required') # # SSL certificate checks # tmp = dict_search('ssl.ca_cert_file', sstp) if not tmp: raise ConfigError(f'SSL CA certificate file required!') else: if not os.path.isfile(tmp): raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!') tmp = dict_search('ssl.cert_file', sstp) if not tmp: raise ConfigError(f'SSL public key file required!') else: if not os.path.isfile(tmp): raise ConfigError(f'SSL public key "{tmp}" does not exist!') tmp = dict_search('ssl.key_file', sstp) if not tmp: raise ConfigError(f'SSL private key file required!') else: if not os.path.isfile(tmp): raise ConfigError(f'SSL private key "{tmp}" does not exist!') def generate(sstp): if not sstp: return None # 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) + render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp) if dict_search('authentication.mode', sstp) == 'local': render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.tmpl', - sstp, trim_blocks=True, permission=0o640) + sstp, permission=0o640) else: if os.path.exists(sstp_chap_secrets): os.unlink(sstp_chap_secrets) return sstp def apply(sstp): if not sstp: call('systemctl stop accel-ppp@sstp.service') for file in [sstp_chap_secrets, sstp_conf]: if os.path.exists(file): os.unlink(file) return None call('systemctl restart accel-ppp@sstp.service') if __name__ == '__main__': try: c = get_config() verify(c) generate(c) apply(c) except ConfigError as e: print(e) exit(1) diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index 172ce71b7..fa19e7d45 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -1,124 +1,124 @@ #!/usr/bin/env python3 # # Copyright (C) 2019 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import argparse import jinja2 import json from sys import exit from tabulate import tabulate from vyos.util import cmd from vyos.config import Config parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") parser.add_argument("-d", "--detail", action="store_true", help="Show detailes LLDP neighbor information on all interfaces") parser.add_argument("-i", "--interface", action="store", help="Show LLDP neighbors on specific interface") # Please be careful if you edit the template. lldp_out = """Capability Codes: R - Router, B - Bridge, W - Wlan r - Repeater, S - Station D - Docsis, T - Telephone, O - Other Device ID Local Proto Cap Platform Port ID --------- ----- ----- --- -------- ------- {% for neighbor in neighbors %} {% for local_if, info in neighbor.items() %} {{ "%-25s" | format(info.chassis) }} {{ "%-9s" | format(local_if) }} {{ "%-6s" | format(info.proto) }} {{ "%-5s" | format(info.capabilities) }} {{ "%-20s" | format(info.platform[:18]) }} {{ info.remote_if }} {% endfor %} {% endfor %} """ def get_neighbors(): return cmd('/usr/sbin/lldpcli -f json show neighbors') def parse_data(data): output = [] for local_if, values in data.items(): for chassis, c_value in values.get('chassis', {}).items(): capabilities = c_value['capability'] if isinstance(capabilities, dict): capabilities = [capabilities] cap = '' for capability in capabilities: if capability['enabled']: if capability['type'] == 'Router': cap += 'R' if capability['type'] == 'Bridge': cap += 'B' if capability['type'] == 'Wlan': cap += 'W' if capability['type'] == 'Station': cap += 'S' if capability['type'] == 'Repeater': cap += 'r' if capability['type'] == 'Telephone': cap += 'T' if capability['type'] == 'Docsis': cap += 'D' if capability['type'] == 'Other': cap += 'O' remote_if = 'Unknown' if 'descr' in values.get('port', {}): remote_if = values.get('port', {}).get('descr') elif 'id' in values.get('port', {}): remote_if = values.get('port', {}).get('id').get('value', 'Unknown') output.append({local_if: {'chassis': chassis, 'remote_if': remote_if, 'proto': values.get('via','Unknown'), 'platform': c_value.get('descr', 'Unknown'), 'capabilities': cap}}) output = {'neighbors': output} return output if __name__ == '__main__': args = parser.parse_args() tmp = { 'neighbors' : [] } c = Config() if not c.exists_effective(['service', 'lldp']): print('Service LLDP is not configured') exit(0) if args.detail: print(cmd('/usr/sbin/lldpctl -f plain')) exit(0) elif args.all or args.interface: tmp = json.loads(get_neighbors()) neighbors = dict() if 'interface' in tmp.get('lldp'): if args.all: neighbors = tmp['lldp']['interface'] elif args.interface: if args.interface in tmp['lldp']['interface']: neighbors[args.interface] = tmp['lldp']['interface'][args.interface] else: parser.print_help() exit(1) - tmpl = jinja2.Template(lldp_out, trim_blocks=True) + tmpl = jinja2.Template(lldp_out) config_text = tmpl.render(parse_data(neighbors)) print(config_text) exit(0)