summaryrefslogtreecommitdiff
path: root/src/conf_mode
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode')
-rwxr-xr-xsrc/conf_mode/arp.py104
-rwxr-xr-xsrc/conf_mode/bcast_relay.py107
-rwxr-xr-xsrc/conf_mode/dhcp_relay.py126
-rwxr-xr-xsrc/conf_mode/dhcp_server.py625
-rwxr-xr-xsrc/conf_mode/dhcpv6_relay.py112
-rwxr-xr-xsrc/conf_mode/dhcpv6_server.py386
-rwxr-xr-xsrc/conf_mode/dns_forwarding.py216
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py249
-rwxr-xr-xsrc/conf_mode/firewall_options.py147
-rwxr-xr-xsrc/conf_mode/flow_accounting_conf.py371
-rwxr-xr-xsrc/conf_mode/host_name.py175
-rwxr-xr-xsrc/conf_mode/http-api.py113
-rwxr-xr-xsrc/conf_mode/https.py185
-rwxr-xr-xsrc/conf_mode/igmp_proxy.py142
-rwxr-xr-xsrc/conf_mode/intel_qat.py103
-rwxr-xr-xsrc/conf_mode/interfaces-bonding.py197
-rwxr-xr-xsrc/conf_mode/interfaces-bridge.py139
-rwxr-xr-xsrc/conf_mode/interfaces-dummy.py73
-rwxr-xr-xsrc/conf_mode/interfaces-ethernet.py89
-rwxr-xr-xsrc/conf_mode/interfaces-geneve.py96
-rwxr-xr-xsrc/conf_mode/interfaces-l2tpv3.py127
-rwxr-xr-xsrc/conf_mode/interfaces-loopback.py61
-rwxr-xr-xsrc/conf_mode/interfaces-macsec.py130
-rwxr-xr-xsrc/conf_mode/interfaces-openvpn.py1116
-rwxr-xr-xsrc/conf_mode/interfaces-pppoe.py132
-rwxr-xr-xsrc/conf_mode/interfaces-pseudo-ethernet.py123
-rwxr-xr-xsrc/conf_mode/interfaces-tunnel.py718
-rwxr-xr-xsrc/conf_mode/interfaces-vxlan.py120
-rwxr-xr-xsrc/conf_mode/interfaces-wireguard.py115
-rwxr-xr-xsrc/conf_mode/interfaces-wireless.py260
-rwxr-xr-xsrc/conf_mode/interfaces-wirelessmodem.py127
-rwxr-xr-xsrc/conf_mode/ipsec-settings.py227
-rwxr-xr-xsrc/conf_mode/le_cert.py115
-rwxr-xr-xsrc/conf_mode/lldp.py249
-rwxr-xr-xsrc/conf_mode/nat.py274
-rwxr-xr-xsrc/conf_mode/ntp.py82
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py216
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py102
-rwxr-xr-xsrc/conf_mode/protocols_igmp.py113
-rwxr-xr-xsrc/conf_mode/protocols_mpls.py187
-rwxr-xr-xsrc/conf_mode/protocols_pim.py140
-rwxr-xr-xsrc/conf_mode/protocols_rip.py317
-rwxr-xr-xsrc/conf_mode/protocols_static_multicast.py117
-rwxr-xr-xsrc/conf_mode/salt-minion.py123
-rwxr-xr-xsrc/conf_mode/service_console-server.py103
-rwxr-xr-xsrc/conf_mode/service_ids_fastnetmon.py89
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py309
-rwxr-xr-xsrc/conf_mode/service_mdns-repeater.py89
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py473
-rwxr-xr-xsrc/conf_mode/service_router-advert.py117
-rwxr-xr-xsrc/conf_mode/snmp.py581
-rwxr-xr-xsrc/conf_mode/ssh.py94
-rwxr-xr-xsrc/conf_mode/system-ip.py85
-rwxr-xr-xsrc/conf_mode/system-ipv6.py113
-rwxr-xr-xsrc/conf_mode/system-login-banner.py110
-rwxr-xr-xsrc/conf_mode/system-login.py403
-rwxr-xr-xsrc/conf_mode/system-options.py109
-rwxr-xr-xsrc/conf_mode/system-proxy.py95
-rwxr-xr-xsrc/conf_mode/system-syslog.py259
-rwxr-xr-xsrc/conf_mode/system-timezone.py57
-rwxr-xr-xsrc/conf_mode/system-wifi-regdom.py87
-rwxr-xr-xsrc/conf_mode/system_console.py141
-rwxr-xr-xsrc/conf_mode/system_lcd.py88
-rwxr-xr-xsrc/conf_mode/task_scheduler.py150
-rwxr-xr-xsrc/conf_mode/tftp_server.py149
-rwxr-xr-xsrc/conf_mode/vpn_anyconnect.py135
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py381
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py286
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py387
-rwxr-xr-xsrc/conf_mode/vrf.py269
-rwxr-xr-xsrc/conf_mode/vrrp.py256
-rwxr-xr-xsrc/conf_mode/vyos_cert.py144
72 files changed, 14605 insertions, 0 deletions
diff --git a/src/conf_mode/arp.py b/src/conf_mode/arp.py
new file mode 100755
index 000000000..aac07bd80
--- /dev/null
+++ b/src/conf_mode/arp.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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
+import os
+import re
+import syslog as sl
+
+from vyos.config import Config
+from vyos.util import call
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+arp_cmd = '/usr/sbin/arp'
+
+def get_config():
+ c = Config()
+ if not c.exists('protocols static arp'):
+ return None
+
+ c.set_level('protocols static')
+ config_data = {}
+
+ for ip_addr in c.list_nodes('arp'):
+ config_data.update(
+ {
+ ip_addr : c.return_value('arp ' + ip_addr + ' hwaddr')
+ }
+ )
+
+ return config_data
+
+def generate(c):
+ c_eff = Config()
+ c_eff.set_level('protocols static')
+ c_eff_cnf = {}
+ for ip_addr in c_eff.list_effective_nodes('arp'):
+ c_eff_cnf.update(
+ {
+ ip_addr : c_eff.return_effective_value('arp ' + ip_addr + ' hwaddr')
+ }
+ )
+
+ config_data = {
+ 'remove' : [],
+ 'update' : {}
+ }
+ ### removal
+ if c == None:
+ for ip_addr in c_eff_cnf:
+ config_data['remove'].append(ip_addr)
+ else:
+ for ip_addr in c_eff_cnf:
+ if not ip_addr in c or c[ip_addr] == None:
+ config_data['remove'].append(ip_addr)
+
+ ### add/update
+ if c != None:
+ for ip_addr in c:
+ if not ip_addr in c_eff_cnf:
+ config_data['update'][ip_addr] = c[ip_addr]
+ if ip_addr in c_eff_cnf:
+ if c[ip_addr] != c_eff_cnf[ip_addr] and c[ip_addr] != None:
+ config_data['update'][ip_addr] = c[ip_addr]
+
+ return config_data
+
+def apply(c):
+ for ip_addr in c['remove']:
+ sl.syslog(sl.LOG_NOTICE, "arp -d " + ip_addr)
+ call(f'{arp_cmd} -d {ip_addr} >/dev/null 2>&1')
+
+ for ip_addr in c['update']:
+ sl.syslog(sl.LOG_NOTICE, "arp -s " + ip_addr + " " + c['update'][ip_addr])
+ updated = c['update'][ip_addr]
+ call(f'{arp_cmd} -s {ip_addr} {updated}')
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ ## syntax verification is done via cli
+ config = generate(c)
+ apply(config)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/bcast_relay.py b/src/conf_mode/bcast_relay.py
new file mode 100755
index 000000000..a3e141a00
--- /dev/null
+++ b/src/conf_mode/bcast_relay.py
@@ -0,0 +1,107 @@
+#!/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():
+ 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)
+
+ 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/dhcp_relay.py b/src/conf_mode/dhcp_relay.py
new file mode 100755
index 000000000..f093a005e
--- /dev/null
+++ b/src/conf_mode/dhcp_relay.py
@@ -0,0 +1,126 @@
+#!/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.template import render
+from vyos.util import call
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-relay/dhcp.conf'
+
+default_config_data = {
+ 'interface': [],
+ 'server': [],
+ 'options': [],
+ 'hop_count': '10',
+ 'relay_agent_packets': 'forward'
+}
+
+def get_config():
+ relay = default_config_data
+ conf = Config()
+ if not conf.exists(['service', 'dhcp-relay']):
+ return None
+ else:
+ conf.set_level(['service', 'dhcp-relay'])
+
+ # Network interfaces to listen on
+ if conf.exists(['interface']):
+ relay['interface'] = conf.return_values(['interface'])
+
+ # Servers equal to the address of the DHCP server(s)
+ if conf.exists(['server']):
+ relay['server'] = conf.return_values(['server'])
+
+ conf.set_level(['service', 'dhcp-relay', 'relay-options'])
+
+ if conf.exists(['hop-count']):
+ count = '-c ' + conf.return_value(['hop-count'])
+ relay['options'].append(count)
+
+ # Specify the maximum packet size to send to a DHCPv4/BOOTP server.
+ # This might be done to allow sufficient space for addition of relay agent
+ # options while still fitting into the Ethernet MTU size.
+ #
+ # Available in DHCPv4 mode only:
+ if conf.exists(['max-size']):
+ size = '-A ' + conf.return_value(['max-size'])
+ relay['options'].append(size)
+
+ # Control the handling of incoming DHCPv4 packets which already contain
+ # relay agent options. If such a packet does not have giaddr set in its
+ # header, the DHCP standard requires that the packet be discarded. However,
+ # if giaddr is set, the relay agent may handle the situation in four ways:
+ # It may append its own set of relay options to the packet, leaving the
+ # supplied option field intact; it may replace the existing agent option
+ # field; it may forward the packet unchanged; or, it may discard it.
+ #
+ # Available in DHCPv4 mode only:
+ if conf.exists(['relay-agents-packets']):
+ pkt = '-a -m ' + conf.return_value(['relay-agents-packets'])
+ relay['options'].append(pkt)
+
+ return relay
+
+def verify(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ if 'lo' in relay['interface']:
+ raise ConfigError('DHCP relay does not support the loopback interface.')
+
+ if len(relay['server']) == 0:
+ raise ConfigError('No DHCP relay server(s) configured.\n' \
+ 'At least one DHCP relay server required.')
+
+ return None
+
+def generate(relay):
+ # bail out early - looks like removal from running config
+ if not relay:
+ return None
+
+ render(config_file, 'dhcp-relay/config.tmpl', relay)
+ return None
+
+def apply(relay):
+ if relay:
+ call('systemctl restart isc-dhcp-relay.service')
+ else:
+ # DHCP relay support is removed in the commit
+ call('systemctl stop isc-dhcp-relay.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ 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/dhcp_server.py b/src/conf_mode/dhcp_server.py
new file mode 100755
index 000000000..0eaa14c5b
--- /dev/null
+++ b/src/conf_mode/dhcp_server.py
@@ -0,0 +1,625 @@
+#!/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 ipaddress import ip_address, ip_network
+from socket import inet_ntoa
+from struct import pack
+from sys import exit
+
+from vyos.config import Config
+from vyos.validate import is_subnet_connected
+from vyos import ConfigError
+from vyos.template import render
+from vyos.util import call, chown
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-server/dhcpd.conf'
+
+default_config_data = {
+ 'disabled': False,
+ 'ddns_enable': False,
+ 'global_parameters': [],
+ 'hostfile_update': False,
+ 'host_decl_name': False,
+ 'static_route': False,
+ 'wpad': False,
+ 'shared_network': [],
+}
+
+def dhcp_slice_range(exclude_list, range_list):
+ """
+ This function is intended to slice a DHCP range. What does it mean?
+
+ Lets assume we have a DHCP range from '192.0.2.1' to '192.0.2.100'
+ but want to exclude address '192.0.2.74' and '192.0.2.75'. We will
+ pass an input 'range_list' in the format:
+ [{'start' : '192.0.2.1', 'stop' : '192.0.2.100' }]
+ and we will receive an output list of:
+ [{'start' : '192.0.2.1' , 'stop' : '192.0.2.73' },
+ {'start' : '192.0.2.76', 'stop' : '192.0.2.100' }]
+ The resulting list can then be used in turn to build the proper dhcpd
+ configuration file.
+ """
+ output = []
+ # exclude list must be sorted for this to work
+ exclude_list = sorted(exclude_list)
+ for ra in range_list:
+ range_start = ra['start']
+ range_stop = ra['stop']
+ range_last_exclude = ''
+
+ for e in exclude_list:
+ if (ip_address(e) >= ip_address(range_start)) and \
+ (ip_address(e) <= ip_address(range_stop)):
+ range_last_exclude = e
+
+ for e in exclude_list:
+ if (ip_address(e) >= ip_address(range_start)) and \
+ (ip_address(e) <= ip_address(range_stop)):
+
+ # Build new IP address range ending one IP address before exclude address
+ r = {
+ 'start' : range_start,
+ 'stop' : str(ip_address(e) -1)
+ }
+ # On the next run our IP address range will start one address after the exclude address
+ range_start = str(ip_address(e) + 1)
+
+ # on subsequent exclude addresses we can not
+ # append them to our output
+ if not (ip_address(r['start']) > ip_address(r['stop'])):
+ # Everything is fine, add range to result
+ output.append(r)
+
+ # Take care of last IP address range spanning from the last exclude
+ # address (+1) to the end of the initial configured range
+ if ip_address(e) == ip_address(range_last_exclude):
+ r = {
+ 'start': str(ip_address(e) + 1),
+ 'stop': str(range_stop)
+ }
+ if not (ip_address(r['start']) > ip_address(r['stop'])):
+ output.append(r)
+ else:
+ # if we have no exclude in the whole range - we just take the range
+ # as it is
+ if not range_last_exclude:
+ if ra not in output:
+ output.append(ra)
+
+ return output
+
+def dhcp_static_route(static_subnet, static_router):
+ # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
+ # Option format is:
+ # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
+ # where bytes with the value 0 are omitted.
+ net = ip_network(static_subnet)
+ # add netmask
+ string = str(net.prefixlen) + ','
+ # add network bytes
+ if net.prefixlen:
+ width = net.prefixlen // 8
+ if net.prefixlen % 8:
+ width += 1
+ string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
+
+ # add router bytes
+ string += ','.join(static_router.split('.'))
+
+ return string
+
+def get_config():
+ dhcp = default_config_data
+ conf = Config()
+ if not conf.exists('service dhcp-server'):
+ return None
+ else:
+ conf.set_level('service dhcp-server')
+
+ # check for global disable of DHCP service
+ if conf.exists('disable'):
+ dhcp['disabled'] = True
+
+ # check for global dynamic DNS upste
+ if conf.exists('dynamic-dns-update'):
+ dhcp['ddns_enable'] = True
+
+ # HACKS AND TRICKS
+ #
+ # check for global 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters from any user
+ if conf.exists('global-parameters'):
+ dhcp['global_parameters'] = conf.return_values('global-parameters')
+
+ # check for global DHCP server updating /etc/host per lease
+ if conf.exists('hostfile-update'):
+ dhcp['hostfile_update'] = True
+
+ # If enabled every host declaration within that scope, the name provided
+ # for the host declaration will be supplied to the client as its hostname.
+ if conf.exists('host-decl-name'):
+ dhcp['host_decl_name'] = True
+
+ # check for multiple, shared networks served with DHCP addresses
+ if conf.exists('shared-network-name'):
+ for network in conf.list_nodes('shared-network-name'):
+ conf.set_level('service dhcp-server shared-network-name {0}'.format(network))
+ config = {
+ 'name': network,
+ 'authoritative': False,
+ 'description': '',
+ 'disabled': False,
+ 'network_parameters': [],
+ 'subnet': []
+ }
+ # check if DHCP server should be authoritative on this network
+ if conf.exists('authoritative'):
+ config['authoritative'] = True
+
+ # A description for this given network
+ if conf.exists('description'):
+ config['description'] = conf.return_value('description')
+
+ # If disabled, the shared-network configuration becomes inactive in
+ # the running DHCP server instance
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('shared-network-parameters'):
+ config['network_parameters'] = conf.return_values('shared-network-parameters')
+
+ # check for multiple subnet configurations in a shared network
+ # config segment
+ if conf.exists('subnet'):
+ for net in conf.list_nodes('subnet'):
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net))
+ subnet = {
+ 'network': net,
+ 'address': str(ip_network(net).network_address),
+ 'netmask': str(ip_network(net).netmask),
+ 'bootfile_name': '',
+ 'bootfile_server': '',
+ 'client_prefix_length': '',
+ 'default_router': '',
+ 'rfc3442_default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'exclude': [],
+ 'failover_local_addr': '',
+ 'failover_name': '',
+ 'failover_peer_addr': '',
+ 'failover_status': '',
+ 'ip_forwarding': False,
+ 'lease': '86400',
+ 'ntp_server': [],
+ 'pop_server': [],
+ 'server_identifier': '',
+ 'smtp_server': [],
+ 'range': [],
+ 'static_mapping': [],
+ 'static_subnet': '',
+ 'static_router': '',
+ 'static_route': '',
+ 'subnet_parameters': [],
+ 'tftp_server': '',
+ 'time_offset': '',
+ 'time_server': [],
+ 'wins_server': [],
+ 'wpad_url': ''
+ }
+
+ # Used to identify a bootstrap file
+ if conf.exists('bootfile-name'):
+ subnet['bootfile_name'] = conf.return_value('bootfile-name')
+
+ # Specify host address of the server from which the initial boot file
+ # (specified above) is to be loaded. Should be a numeric IP address or
+ # domain name.
+ if conf.exists('bootfile-server'):
+ subnet['bootfile_server'] = conf.return_value('bootfile-server')
+
+ # The subnet mask option specifies the client's subnet mask as per RFC 950. If no subnet
+ # mask option is provided anywhere in scope, as a last resort dhcpd will use the subnet
+ # mask from the subnet declaration for the network on which an address is being assigned.
+ if conf.exists('client-prefix-length'):
+ # snippet borrowed from https://stackoverflow.com/questions/33750233/convert-cidr-to-subnet-mask-in-python
+ host_bits = 32 - int(conf.return_value('client-prefix-length'))
+ subnet['client_prefix_length'] = inet_ntoa(pack('!I', (1 << 32) - (1 << host_bits)))
+
+ # Default router IP address on the client's subnet
+ if conf.exists('default-router'):
+ subnet['default_router'] = conf.return_value('default-router')
+ subnet['rfc3442_default_router'] = dhcp_static_route("0.0.0.0/0", subnet['default_router'])
+
+ # Specifies a list of Domain Name System (STD 13, RFC 1035) name servers available to
+ # the client. Servers should be listed in order of preference.
+ if conf.exists('dns-server'):
+ subnet['dns_server'] = conf.return_values('dns-server')
+
+ # Option specifies the domain name that client should use when resolving hostnames
+ # via the Domain Name System.
+ if conf.exists('domain-name'):
+ subnet['domain_name'] = conf.return_value('domain-name')
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists('domain-search'):
+ for domain in conf.return_values('domain-search'):
+ subnet['domain_search'].append('"' + domain + '"')
+
+ # IP address (local) for failover peer to connect
+ if conf.exists('failover local-address'):
+ subnet['failover_local_addr'] = conf.return_value('failover local-address')
+
+ # DHCP failover peer name
+ if conf.exists('failover name'):
+ subnet['failover_name'] = conf.return_value('failover name')
+
+ # IP address (remote) of failover peer
+ if conf.exists('failover peer-address'):
+ subnet['failover_peer_addr'] = conf.return_value('failover peer-address')
+
+ # DHCP failover peer status (primary|secondary)
+ if conf.exists('failover status'):
+ subnet['failover_status'] = conf.return_value('failover status')
+
+ # Option specifies whether the client should configure its IP layer for packet
+ # forwarding
+ if conf.exists('ip-forwarding'):
+ subnet['ip_forwarding'] = True
+
+ # Time should be the length in seconds that will be assigned to a lease if the
+ # client requesting the lease does not ask for a specific expiration time
+ if conf.exists('lease'):
+ subnet['lease'] = conf.return_value('lease')
+
+ # Specifies a list of IP addresses indicating NTP (RFC 5905) servers available
+ # to the client.
+ if conf.exists('ntp-server'):
+ subnet['ntp_server'] = conf.return_values('ntp-server')
+
+ # POP3 server option specifies a list of POP3 servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('pop-server'):
+ subnet['pop_server'] = conf.return_values('pop-server')
+
+ # DHCP servers include this option in the DHCPOFFER in order to allow the client
+ # to distinguish between lease offers. DHCP clients use the contents of the
+ # 'server identifier' field as the destination address for any DHCP messages
+ # unicast to the DHCP server
+ if conf.exists('server-identifier'):
+ subnet['server_identifier'] = conf.return_value('server-identifier')
+
+ # SMTP server option specifies a list of SMTP servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('smtp-server'):
+ subnet['smtp_server'] = conf.return_values('smtp-server')
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one range statement. The range statement gives the lowest and highest IP
+ # addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists('range'):
+ for range in conf.list_nodes('range'):
+ range = {
+ 'start': conf.return_value('range {0} start'.format(range)),
+ 'stop': conf.return_value('range {0} stop'.format(range))
+ }
+ subnet['range'].append(range)
+
+ # IP address that needs to be excluded from DHCP lease range
+ if conf.exists('exclude'):
+ subnet['exclude'] = conf.return_values('exclude')
+ subnet['range'] = dhcp_slice_range(subnet['exclude'], subnet['range'])
+
+ # Static DHCP leases
+ if conf.exists('static-mapping'):
+ addresses_for_exclude = []
+ for mapping in conf.list_nodes('static-mapping'):
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1} static-mapping {2}'.format(network, net, mapping))
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ip_address': '',
+ 'mac_address': '',
+ 'static_parameters': []
+ }
+
+ # This static lease is disabled
+ if conf.exists('disable'):
+ mapping['disabled'] = True
+
+ # IP address used for this DHCP client
+ if conf.exists('ip-address'):
+ mapping['ip_address'] = conf.return_value('ip-address')
+ addresses_for_exclude.append(mapping['ip_address'])
+
+ # MAC address of requesting DHCP client
+ if conf.exists('mac-address'):
+ mapping['mac_address'] = conf.return_value('mac-address')
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('static-mapping-parameters'):
+ mapping['static_parameters'] = conf.return_values('static-mapping-parameters')
+
+ # append static-mapping configuration to subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # Now we have all static DHCP leases - we also need to slice them
+ # out of our DHCP ranges to avoid ISC DHCPd warnings as:
+ # dhcpd: Dynamic and static leases present for 192.0.2.51.
+ # dhcpd: Remove host declaration DMZ_PC1 or remove 192.0.2.51
+ # dhcpd: from the dynamic address pool for DMZ
+ subnet['range'] = dhcp_slice_range(addresses_for_exclude, subnet['range'])
+
+ # Reset config level to matching hirachy
+ conf.set_level('service dhcp-server shared-network-name {0} subnet {1}'.format(network, net))
+
+ # This option specifies a list of static routes that the client should install in its routing
+ # cache. If multiple routes to the same destination are specified, they are listed in descending
+ # order of priority.
+ if conf.exists('static-route destination-subnet'):
+ subnet['static_subnet'] = conf.return_value('static-route destination-subnet')
+ # Required for global config section
+ dhcp['static_route'] = True
+
+ if conf.exists('static-route router'):
+ subnet['static_router'] = conf.return_value('static-route router')
+
+ if subnet['static_router'] and subnet['static_subnet']:
+ subnet['static_route'] = dhcp_static_route(subnet['static_subnet'], subnet['static_router'])
+
+ # HACKS AND TRICKS
+ #
+ # check for 'raw' ISC DHCP parameters configured by users
+ # actually this is a bad idea in general to pass raw parameters
+ # from any user
+ #
+ # deprecate this and issue a warning like we do for DNS forwarding?
+ if conf.exists('subnet-parameters'):
+ subnet['subnet_parameters'] = conf.return_values('subnet-parameters')
+
+ # This option is used to identify a TFTP server and, if supported by the client, should have
+ # the same effect as the server-name declaration. BOOTP clients are unlikely to support this
+ # option. Some DHCP clients will support it, and others actually require it.
+ if conf.exists('tftp-server-name'):
+ subnet['tftp_server'] = conf.return_value('tftp-server-name')
+
+ # The time-offset option specifies the offset of the client’s subnet in seconds from
+ # Coordinated Universal Time (UTC).
+ if conf.exists('time-offset'):
+ subnet['time_offset'] = conf.return_value('time-offset')
+
+ # The time-server option specifies a list of RFC 868 time servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists('time-server'):
+ subnet['time_server'] = conf.return_values('time-server')
+
+ # The NetBIOS name server (NBNS) option specifies a list of RFC 1001/1002 NBNS name servers
+ # listed in order of preference. NetBIOS Name Service is currently more commonly referred to
+ # as WINS. WINS servers can be specified using the netbios-name-servers option.
+ if conf.exists('wins-server'):
+ subnet['wins_server'] = conf.return_values('wins-server')
+
+ # URL for Web Proxy Autodiscovery Protocol
+ if conf.exists('wpad-url'):
+ subnet['wpad_url'] = conf.return_value('wpad-url')
+ # Required for global config section
+ dhcp['wpad'] = True
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+ # append shared network configuration to config dictionary
+ dhcp['shared_network'].append(config)
+
+ return dhcp
+
+def verify(dhcp):
+ if not dhcp or dhcp['disabled']:
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcp['shared_network']) == 0:
+ raise ConfigError('No DHCP shared networks configured.\n' \
+ 'At least one DHCP shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ failover_names = []
+ listen_ok = False
+ subnets = []
+
+ # A shared-network requires a subnet definition
+ for network in dhcp['shared_network']:
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCP lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ for subnet in network['subnet']:
+ # Subnet static route declaration requires destination and router
+ if subnet['static_subnet'] or subnet['static_router']:
+ if not (subnet['static_subnet'] and subnet['static_router']):
+ raise ConfigError('Please specify missing DHCP static-route parameter(s):\n' \
+ 'destination-subnet | router')
+
+ # Failover requires all 4 parameters set
+ if subnet['failover_local_addr'] or subnet['failover_peer_addr'] or subnet['failover_name'] or subnet['failover_status']:
+ if not (subnet['failover_local_addr'] and subnet['failover_peer_addr'] and subnet['failover_name'] and subnet['failover_status']):
+ raise ConfigError('Please specify missing DHCP failover parameter(s):\n' \
+ 'local-address | peer-address | name | status')
+
+ # Failover names must be uniquie
+ if subnet['failover_name'] in failover_names:
+ raise ConfigError('Failover names must be unique:\n' \
+ '{0} has already been configured!'.format(subnet['failover_name']))
+ else:
+ failover_names.append(subnet['failover_name'])
+
+ # Failover requires start/stop ranges for pool
+ if (len(subnet['range']) == 0):
+ raise ConfigError('At least one start-stop range must be configured for {0}\n' \
+ 'to set up DHCP failover!'.format(subnet['network']))
+
+ # Check if DHCP address range is inside configured subnet declaration
+ range_start = []
+ range_stop = []
+ for range in subnet['range']:
+ start = range['start']
+ stop = range['stop']
+ # DHCP stop IP required after start IP
+ if start and not stop:
+ raise ConfigError('DHCP range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ip_address(start) in ip_network(subnet['network']):
+ raise ConfigError('DHCP range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ip_address(stop) in ip_network(subnet['network']):
+ raise ConfigError('DHCP range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ip_address(stop) >= ip_address(start):
+ raise ConfigError('DHCP range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # Range start address must be unique
+ if start in range_start:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range_start.append(start)
+
+ # Range stop address must be unique
+ if stop in range_stop:
+ raise ConfigError('Conflicting DHCP lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range_stop.append(stop)
+
+ # Exclude addresses must be in bound
+ for exclude in subnet['exclude']:
+ if not ip_address(exclude) in ip_network(subnet['network']):
+ raise ConfigError('Exclude IP address {0} is outside of the DHCP lease network {1}\n' \
+ 'under shared network {2}!'.format(exclude, subnet['network'], network['name']))
+
+ # At least one DHCP address range or static-mapping required
+ active_mapping = False
+ if (len(subnet['range']) == 0):
+ for mapping in subnet['static_mapping']:
+ # we need at least one active mapping
+ if (not active_mapping) and (not mapping['disabled']):
+ active_mapping = True
+ else:
+ active_mapping = True
+
+ if not active_mapping:
+ raise ConfigError('No DHCP address range or active static-mapping set\n' \
+ 'for subnet {0}!'.format(subnet['network']))
+
+ # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set)
+ for mapping in subnet['static_mapping']:
+
+ if mapping['ip_address']:
+ # Static IP address must be in bound
+ if not ip_address(mapping['ip_address']) in ip_network(subnet['network']):
+ raise ConfigError('DHCP static lease IP address {0} for static mapping {1}\n' \
+ 'in shared network {2} is outside DHCP lease subnet {3}!' \
+ .format(mapping['ip_address'], mapping['name'], network['name'], subnet['network']))
+
+ # Static mapping requires MAC address
+ if not mapping['mac_address']:
+ raise ConfigError('DHCP static lease MAC address not specified for static mapping\n' \
+ '{0} under shared network name {1}!'.format(mapping['name'], network['name']))
+
+ # There must be one subnet connected to a listen interface.
+ # This only counts if the network itself is not disabled!
+ if not network['disabled']:
+ if is_subnet_connected(subnet['network'], primary=True):
+ listen_ok = True
+
+ # Subnets must be non overlapping
+ if subnet['network'] in subnets:
+ raise ConfigError('DHCP subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ else:
+ subnets.append(subnet['network'])
+
+ # Check for overlapping subnets
+ net = ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCP conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('DHCP server configuration error!\n' \
+ 'None of configured DHCP subnets does not have appropriate\n' \
+ 'primary IP address on any broadcast interface.')
+
+ return None
+
+def generate(dhcp):
+ if not dhcp or dhcp['disabled']:
+ return None
+
+ # Please see: https://phabricator.vyos.net/T1129 for quoting of the raw parameters
+ # we can pass to ISC DHCPd
+ render(config_file, 'dhcp-server/dhcpd.conf.tmpl', dhcp,
+ formater=lambda _: _.replace("&quot;", '"'))
+ return None
+
+def apply(dhcp):
+ if not dhcp or dhcp['disabled']:
+ # DHCP server is removed in the commit
+ call('systemctl stop isc-dhcp-server.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ return None
+
+ call('systemctl restart isc-dhcp-server.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/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py
new file mode 100755
index 000000000..6ef290bf0
--- /dev/null
+++ b/src/conf_mode/dhcpv6_relay.py
@@ -0,0 +1,112 @@
+#!/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 copy import deepcopy
+
+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'/run/dhcp-relay/dhcpv6.conf'
+
+default_config_data = {
+ 'listen_addr': [],
+ 'upstream_addr': [],
+ 'options': [],
+}
+
+def get_config():
+ relay = deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists('service dhcpv6-relay'):
+ return None
+ else:
+ conf.set_level('service dhcpv6-relay')
+
+ # Network interfaces/address to listen on for DHCPv6 query(s)
+ if conf.exists('listen-interface'):
+ interfaces = conf.list_nodes('listen-interface')
+ for intf in interfaces:
+ if conf.exists('listen-interface {0} address'.format(intf)):
+ addr = conf.return_value('listen-interface {0} address'.format(intf))
+ listen = addr + '%' + intf
+ relay['listen_addr'].append(listen)
+
+ # Upstream interface/address for remote DHCPv6 server
+ if conf.exists('upstream-interface'):
+ interfaces = conf.list_nodes('upstream-interface')
+ for intf in interfaces:
+ addresses = conf.return_values('upstream-interface {0} address'.format(intf))
+ for addr in addresses:
+ server = addr + '%' + intf
+ relay['upstream_addr'].append(server)
+
+ # Maximum hop count. When forwarding packets, dhcrelay discards packets
+ # which have reached a hop count of COUNT. Default is 10. Maximum is 255.
+ if conf.exists('max-hop-count'):
+ count = '-c ' + conf.return_value('max-hop-count')
+ relay['options'].append(count)
+
+ if conf.exists('use-interface-id-option'):
+ relay['options'].append('-I')
+
+ return relay
+
+def verify(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ if len(relay['listen_addr']) == 0 or len(relay['upstream_addr']) == 0:
+ raise ConfigError('Must set at least one listen and upstream interface addresses.')
+
+ return None
+
+def generate(relay):
+ # bail out early - looks like removal from running config
+ if relay is None:
+ return None
+
+ render(config_file, 'dhcpv6-relay/config.tmpl', relay)
+ return None
+
+def apply(relay):
+ if relay is not None:
+ call('systemctl restart isc-dhcp-relay6.service')
+ else:
+ # DHCPv6 relay support is removed in the commit
+ call('systemctl stop isc-dhcp-relay6.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ 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/dhcpv6_server.py b/src/conf_mode/dhcpv6_server.py
new file mode 100755
index 000000000..53c8358a5
--- /dev/null
+++ b/src/conf_mode/dhcpv6_server.py
@@ -0,0 +1,386 @@
+#!/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 ipaddress
+
+from sys import exit
+from copy import deepcopy
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_subnet_connected, is_ipv6
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/dhcp-server/dhcpdv6.conf'
+
+default_config_data = {
+ 'preference': '',
+ 'disabled': False,
+ 'shared_network': []
+}
+
+def get_config():
+ dhcpv6 = deepcopy(default_config_data)
+ conf = Config()
+ base = ['service', 'dhcpv6-server']
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ # Check for global disable of DHCPv6 service
+ if conf.exists(['disable']):
+ dhcpv6['disabled'] = True
+ return dhcpv6
+
+ # Preference of this DHCPv6 server compared with others
+ if conf.exists(['preference']):
+ dhcpv6['preference'] = conf.return_value(['preference'])
+
+ # check for multiple, shared networks served with DHCPv6 addresses
+ if conf.exists(['shared-network-name']):
+ for network in conf.list_nodes(['shared-network-name']):
+ conf.set_level(base + ['shared-network-name', network])
+ config = {
+ 'name': network,
+ 'disabled': False,
+ 'subnet': []
+ }
+
+ # If disabled, the shared-network configuration becomes inactive
+ if conf.exists(['disable']):
+ config['disabled'] = True
+
+ # check for multiple subnet configurations in a shared network
+ if conf.exists(['subnet']):
+ for net in conf.list_nodes(['subnet']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ subnet = {
+ 'network': net,
+ 'range6_prefix': [],
+ 'range6': [],
+ 'default_router': '',
+ 'dns_server': [],
+ 'domain_name': '',
+ 'domain_search': [],
+ 'lease_def': '',
+ 'lease_min': '',
+ 'lease_max': '',
+ 'nis_domain': '',
+ 'nis_server': [],
+ 'nisp_domain': '',
+ 'nisp_server': [],
+ 'prefix_delegation': [],
+ 'sip_address': [],
+ 'sip_hostname': [],
+ 'sntp_server': [],
+ 'static_mapping': []
+ }
+
+ # For any subnet on which addresses will be assigned dynamically, there must be at
+ # least one address range statement. The range statement gives the lowest and highest
+ # IP addresses in a range. All IP addresses in the range should be in the subnet in
+ # which the range statement is declared.
+ if conf.exists(['address-range', 'prefix']):
+ for prefix in conf.list_nodes(['address-range', 'prefix']):
+ range = {
+ 'prefix': prefix,
+ 'temporary': False
+ }
+
+ # Address range will be used for temporary addresses
+ if conf.exists(['address-range' 'prefix', prefix, 'temporary']):
+ range['temporary'] = True
+
+ # Append to subnet temporary range6 list
+ subnet['range6_prefix'].append(range)
+
+ if conf.exists(['address-range', 'start']):
+ for range in conf.list_nodes(['address-range', 'start']):
+ range = {
+ 'start': range,
+ 'stop': conf.return_value(['address-range', 'start', range, 'stop'])
+ }
+
+ # Append to subnet range6 list
+ subnet['range6'].append(range)
+
+ # The domain-search option specifies a 'search list' of Domain Names to be used
+ # by the client to locate not-fully-qualified domain names.
+ if conf.exists(['domain-search']):
+ subnet['domain_search'] = conf.return_values(['domain-search'])
+
+ # IPv6 address valid lifetime
+ # (at the end the address is no longer usable by the client)
+ # (set to 30 days, the usual IPv6 default)
+ if conf.exists(['lease-time', 'default']):
+ subnet['lease_def'] = conf.return_value(['lease-time', 'default'])
+
+ # Time should be the maximum length in seconds that will be assigned to a lease.
+ # The only exception to this is that Dynamic BOOTP lease lengths, which are not
+ # specified by the client, are not limited by this maximum.
+ if conf.exists(['lease-time', 'maximum']):
+ subnet['lease_max'] = conf.return_value(['lease-time', 'maximum'])
+
+ # Time should be the minimum length in seconds that will be assigned to a lease
+ if conf.exists(['lease-time', 'minimum']):
+ subnet['lease_min'] = conf.return_value(['lease-time', 'minimum'])
+
+ # Specifies a list of Domain Name System name servers available to the client.
+ # Servers should be listed in order of preference.
+ if conf.exists(['name-server']):
+ subnet['dns_server'] = conf.return_values(['name-server'])
+
+ # Ancient NIS (Network Information Service) domain name
+ if conf.exists(['nis-domain']):
+ subnet['nis_domain'] = conf.return_value(['nis-domain'])
+
+ # Ancient NIS (Network Information Service) servers
+ if conf.exists(['nis-server']):
+ subnet['nis_server'] = conf.return_values(['nis-server'])
+
+ # Ancient NIS+ (Network Information Service) domain name
+ if conf.exists(['nisplus-domain']):
+ subnet['nisp_domain'] = conf.return_value(['nisplus-domain'])
+
+ # Ancient NIS+ (Network Information Service) servers
+ if conf.exists(['nisplus-server']):
+ subnet['nisp_server'] = conf.return_values(['nisplus-server'])
+
+ # Local SIP server that is to be used for all outbound SIP requests - IPv6 address
+ if conf.exists(['sip-server']):
+ for value in conf.return_values(['sip-server']):
+ if is_ipv6(value):
+ subnet['sip_address'].append(value)
+ else:
+ subnet['sip_hostname'].append(value)
+
+ # List of local SNTP servers available for the client to synchronize their clocks
+ if conf.exists(['sntp-server']):
+ subnet['sntp_server'] = conf.return_values(['sntp-server'])
+
+ # Prefix Delegation (RFC 3633)
+ if conf.exists(['prefix-delegation', 'start']):
+ for address in conf.list_nodes(['prefix-delegation', 'start']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'prefix-delegation', 'start', address])
+ prefix = {
+ 'start' : address,
+ 'stop' : '',
+ 'length' : ''
+ }
+
+ if conf.exists(['prefix-length']):
+ prefix['length'] = conf.return_value(['prefix-length'])
+
+ if conf.exists(['stop']):
+ prefix['stop'] = conf.return_value(['stop'])
+
+ subnet['prefix_delegation'].append(prefix)
+
+ #
+ # Static DHCP v6 leases
+ #
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net])
+ if conf.exists(['static-mapping']):
+ for mapping in conf.list_nodes(['static-mapping']):
+ conf.set_level(base + ['shared-network-name', network, 'subnet', net, 'static-mapping', mapping])
+ mapping = {
+ 'name': mapping,
+ 'disabled': False,
+ 'ipv6_address': '',
+ 'client_identifier': '',
+ }
+
+ # This static lease is disabled
+ if conf.exists(['disable']):
+ mapping['disabled'] = True
+
+ # IPv6 address used for this DHCP client
+ if conf.exists(['ipv6-address']):
+ mapping['ipv6_address'] = conf.return_value(['ipv6-address'])
+
+ # This option specifies the client’s DUID identifier. DUIDs are similar but different from DHCPv4 client identifiers
+ if conf.exists(['identifier']):
+ mapping['client_identifier'] = conf.return_value(['identifier'])
+
+ # append static mapping configuration tu subnet list
+ subnet['static_mapping'].append(mapping)
+
+ # append subnet configuration to shared network subnet list
+ config['subnet'].append(subnet)
+
+ # append shared network configuration to config dictionary
+ dhcpv6['shared_network'].append(config)
+
+ # If all shared-networks are disabled, there's nothing to do.
+ if all(net['disabled'] for net in dhcpv6['shared_network']):
+ return None
+
+ return dhcpv6
+
+def verify(dhcpv6):
+ if not dhcpv6 or dhcpv6['disabled']:
+ return None
+
+ # If DHCP is enabled we need one share-network
+ if len(dhcpv6['shared_network']) == 0:
+ raise ConfigError('No DHCPv6 shared networks configured.\n' \
+ 'At least one DHCPv6 shared network must be configured.')
+
+ # Inspect shared-network/subnet
+ subnets = []
+ listen_ok = False
+
+ for network in dhcpv6['shared_network']:
+ # A shared-network requires a subnet definition
+ if len(network['subnet']) == 0:
+ raise ConfigError('No DHCPv6 lease subnets configured for {0}. At least one\n' \
+ 'lease subnet must be configured for each shared network.'.format(network['name']))
+
+ range6_start = []
+ range6_stop = []
+ for subnet in network['subnet']:
+ # Ususal range declaration with a start and stop address
+ for range6 in subnet['range6']:
+ # shorten names
+ start = range6['start']
+ stop = range6['stop']
+
+ # DHCPv6 stop address is required
+ if start and not stop:
+ raise ConfigError('DHCPv6 range stop address for start {0} is not defined!'.format(start))
+
+ # Start address must be inside network
+ if not ipaddress.ip_address(start) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range start address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(start, subnet['network'], network['name']))
+
+ # Stop address must be inside network
+ if not ipaddress.ip_address(stop) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 range stop address {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(stop, subnet['network'], network['name']))
+
+ # Stop address must be greater or equal to start address
+ if not ipaddress.ip_address(stop) >= ipaddress.ip_address(start):
+ raise ConfigError('DHCPv6 range stop address {0} must be greater or equal\n' \
+ 'to the range start address {1}!'.format(stop, start))
+
+ # DHCPv6 range start address must be unique - two ranges can't
+ # start with the same address - makes no sense
+ if start in range6_start:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool start address {0} defined multipe times!'.format(start))
+ else:
+ range6_start.append(start)
+
+ # DHCPv6 range stop address must be unique - two ranges can't
+ # end with the same address - makes no sense
+ if stop in range6_stop:
+ raise ConfigError('Conflicting DHCPv6 lease range:\n' \
+ 'Pool stop address {0} defined multipe times!'.format(stop))
+ else:
+ range6_stop.append(stop)
+
+ # Prefix delegation sanity checks
+ for prefix in subnet['prefix_delegation']:
+ if not prefix['stop']:
+ raise ConfigError('Stop address of delegated IPv6 prefix range must be configured')
+
+ if not prefix['length']:
+ raise ConfigError('Length of delegated IPv6 prefix must be configured')
+
+ # We also have prefixes that require checking
+ for prefix in subnet['range6_prefix']:
+ # If configured prefix does not match our subnet, we have to check that it's inside
+ if ipaddress.ip_network(prefix['prefix']) != ipaddress.ip_network(subnet['network']):
+ # Configured prefixes must be inside our network
+ if not ipaddress.ip_network(prefix['prefix']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 prefix {0} is not in subnet {1}\n' \
+ 'specified for shared network {2}!'.format(prefix['prefix'], subnet['network'], network['name']))
+
+ # Static mappings don't require anything (but check if IP is in subnet if it's set)
+ for mapping in subnet['static_mapping']:
+ if mapping['ipv6_address']:
+ # Static address must be in subnet
+ if not ipaddress.ip_address(mapping['ipv6_address']) in ipaddress.ip_network(subnet['network']):
+ raise ConfigError('DHCPv6 static mapping IPv6 address {0} for static mapping {1}\n' \
+ 'in shared network {2} is outside subnet {3}!' \
+ .format(mapping['ipv6_address'], mapping['name'], network['name'], subnet['network']))
+
+ # Subnets must be unique
+ if subnet['network'] in subnets:
+ raise ConfigError('DHCPv6 subnets must be unique! Subnet {0} defined multiple times!'.format(subnet['network']))
+ else:
+ subnets.append(subnet['network'])
+
+ # DHCPv6 requires at least one configured address range or one static mapping
+ # (FIXME: is not actually checked right now?)
+
+ # There must be one subnet connected to a listen interface if network is not disabled.
+ if not network['disabled']:
+ if is_subnet_connected(subnet['network']):
+ listen_ok = True
+
+ # DHCPv6 subnet must not overlap. ISC DHCP also complains about overlapping
+ # subnets: "Warning: subnet 2001:db8::/32 overlaps subnet 2001:db8:1::/32"
+ net = ipaddress.ip_network(subnet['network'])
+ for n in subnets:
+ net2 = ipaddress.ip_network(n)
+ if (net != net2):
+ if net.overlaps(net2):
+ raise ConfigError('DHCPv6 conflicting subnet ranges: {0} overlaps {1}'.format(net, net2))
+
+ if not listen_ok:
+ raise ConfigError('None of the DHCPv6 subnets are connected to a subnet6 on\n' \
+ 'this machine. At least one subnet6 must be connected such that\n' \
+ 'DHCPv6 listens on an interface!')
+
+
+ return None
+
+def generate(dhcpv6):
+ if not dhcpv6 or dhcpv6['disabled']:
+ return None
+
+ render(config_file, 'dhcpv6-server/dhcpdv6.conf.tmpl', dhcpv6)
+ return None
+
+def apply(dhcpv6):
+ if not dhcpv6 or dhcpv6['disabled']:
+ # DHCP server is removed in the commit
+ call('systemctl stop isc-dhcp-server6.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+
+ else:
+ call('systemctl restart isc-dhcp-server6.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
new file mode 100755
index 000000000..51631dc16
--- /dev/null
+++ b/src/conf_mode/dns_forwarding.py
@@ -0,0 +1,216 @@
+#!/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 copy import deepcopy
+
+from vyos.config import Config
+from vyos.hostsd_client import Client as hostsd_client
+from vyos import ConfigError
+from vyos.util import call, chown
+from vyos.template import render
+
+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'
+
+default_config_data = {
+ 'allow_from': [],
+ 'cache_size': 10000,
+ 'export_hosts_file': 'yes',
+ 'listen_address': [],
+ 'name_servers': [],
+ 'negative_ttl': 3600,
+ 'system': False,
+ 'domains': {},
+ 'dnssec': 'process-no-validate',
+ 'dhcp_interfaces': []
+}
+
+hostsd_tag = 'static'
+
+def get_config(conf):
+ dns = deepcopy(default_config_data)
+ base = ['service', 'dns', 'forwarding']
+
+ if not conf.exists(base):
+ return None
+
+ conf.set_level(base)
+
+ if conf.exists(['allow-from']):
+ dns['allow_from'] = conf.return_values(['allow-from'])
+
+ if conf.exists(['cache-size']):
+ cache_size = conf.return_value(['cache-size'])
+ dns['cache_size'] = cache_size
+
+ if conf.exists('negative-ttl'):
+ negative_ttl = conf.return_value(['negative-ttl'])
+ dns['negative_ttl'] = negative_ttl
+
+ if conf.exists(['domain']):
+ for domain in conf.list_nodes(['domain']):
+ conf.set_level(base + ['domain', domain])
+ entry = {
+ 'nslist': bracketize_ipv6_addrs(conf.return_values(['server'])),
+ 'addNTA': conf.exists(['addnta']),
+ 'recursion-desired': conf.exists(['recursion-desired'])
+ }
+ dns['domains'][domain] = entry
+
+ conf.set_level(base)
+
+ if conf.exists(['ignore-hosts-file']):
+ dns['export_hosts_file'] = "no"
+
+ if conf.exists(['name-server']):
+ dns['name_servers'] = bracketize_ipv6_addrs(
+ conf.return_values(['name-server']))
+
+ if conf.exists(['system']):
+ dns['system'] = True
+
+ if conf.exists(['listen-address']):
+ dns['listen_address'] = conf.return_values(['listen-address'])
+
+ if conf.exists(['dnssec']):
+ dns['dnssec'] = conf.return_value(['dnssec'])
+
+ if conf.exists(['dhcp']):
+ dns['dhcp_interfaces'] = conf.return_values(['dhcp'])
+
+ return dns
+
+def bracketize_ipv6_addrs(addrs):
+ """Wraps each IPv6 addr in addrs in [], leaving IPv4 addrs untouched."""
+ return ['[{0}]'.format(a) if a.count(':') > 1 else a for a in addrs]
+
+def verify(conf, dns):
+ # bail out early - looks like removal from running config
+ if dns is None:
+ return None
+
+ if not dns['listen_address']:
+ raise ConfigError(
+ "Error: DNS forwarding requires a listen-address")
+
+ if not dns['allow_from']:
+ raise ConfigError(
+ "Error: DNS forwarding requires an allow-from network")
+
+ if dns['domains']:
+ for domain in dns['domains']:
+ if not dns['domains'][domain]['nslist']:
+ raise ConfigError((
+ f'Error: No server configured for domain {domain}'))
+
+ no_system_nameservers = False
+ if dns['system'] and not (
+ conf.exists(['system', 'name-server']) or
+ conf.exists(['system', 'name-servers-dhcp']) ):
+ no_system_nameservers = True
+ print(("DNS forwarding warning: No 'system name-server' or "
+ "'system name-servers-dhcp' set\n"))
+
+ if (no_system_nameservers or not dns['system']) and not (
+ dns['name_servers'] or dns['dhcp_interfaces']):
+ print(("DNS forwarding warning: No 'dhcp', 'name-server' or 'system' "
+ "nameservers set. Forwarding will operate as a recursor.\n"))
+
+ return None
+
+def generate(dns):
+ # bail out early - looks like removal from running config
+ if dns is None:
+ return None
+
+ render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.tmpl',
+ dns, user=pdns_rec_user, group=pdns_rec_group)
+
+ render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.tmpl',
+ dns, user=pdns_rec_user, group=pdns_rec_group)
+
+ # if vyos-hostsd didn't create its files yet, create them (empty)
+ for f in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]:
+ with open(f, 'a'):
+ pass
+ chown(f, user=pdns_rec_user, group=pdns_rec_group)
+
+ return None
+
+def apply(dns):
+ if dns is None:
+ # 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 dns['name_servers']:
+ hc.add_name_servers({hostsd_tag: dns['name_servers']})
+
+ # 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 dns['system']:
+ hc.add_name_server_tags_recursor(['system'])
+ else:
+ hc.delete_name_server_tags_recursor(['system'])
+
+ # add dhcp nameserver tags for configured interfaces
+ for intf in dns['dhcp_interfaces']:
+ hc.add_name_server_tags_recursor(['dhcp-' + intf, 'dhcpv6-' + intf ])
+
+ # 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 dns['domains']:
+ hc.add_forward_zones(dns['domains'])
+
+ # 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:
+ conf = Config()
+ c = get_config(conf)
+ verify(conf, 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
new file mode 100755
index 000000000..5b1883c03
--- /dev/null
+++ b/src/conf_mode/dynamic_dns.py
@@ -0,0 +1,249 @@
+#!/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 copy import deepcopy
+from stat import S_IRUSR, S_IWUSR
+
+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'/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'
+}
+
+default_config_data = {
+ 'interfaces': [],
+ 'deleted': False
+}
+
+def get_config():
+ dyndns = deepcopy(default_config_data)
+ conf = Config()
+ base_level = ['service', 'dns', 'dynamic']
+
+ if not conf.exists(base_level):
+ dyndns['deleted'] = True
+ return dyndns
+
+ for interface in conf.list_nodes(base_level + ['interface']):
+ node = {
+ 'interface': interface,
+ 'rfc2136': [],
+ 'service': [],
+ 'web_skip': '',
+ 'web_url': ''
+ }
+
+ # set config level to e.g. "service dns dynamic interface eth0"
+ conf.set_level(base_level + ['interface', interface])
+ # Handle RFC2136 - Dynamic Updates in the Domain Name System
+ for rfc2136 in conf.list_nodes(['rfc2136']):
+ rfc = {
+ 'name': rfc2136,
+ 'keyfile': '',
+ 'record': [],
+ 'server': '',
+ 'ttl': '600',
+ 'zone': ''
+ }
+
+ # set config level
+ conf.set_level(base_level + ['interface', interface, 'rfc2136', rfc2136])
+
+ if conf.exists(['key']):
+ rfc['keyfile'] = conf.return_value(['key'])
+
+ if conf.exists(['record']):
+ rfc['record'] = conf.return_values(['record'])
+
+ if conf.exists(['server']):
+ rfc['server'] = conf.return_value(['server'])
+
+ if conf.exists(['ttl']):
+ rfc['ttl'] = conf.return_value(['ttl'])
+
+ if conf.exists(['zone']):
+ rfc['zone'] = conf.return_value(['zone'])
+
+ node['rfc2136'].append(rfc)
+
+ # set config level to e.g. "service dns dynamic interface eth0"
+ conf.set_level(base_level + ['interface', interface])
+ # Handle DynDNS service providers
+ for service in conf.list_nodes(['service']):
+ srv = {
+ 'provider': service,
+ 'host': [],
+ 'login': '',
+ 'password': '',
+ 'protocol': '',
+ 'server': '',
+ 'custom' : False,
+ 'zone' : ''
+ }
+
+ # set config level
+ conf.set_level(base_level + ['interface', interface, 'service', service])
+
+ # preload protocol from default service mapping
+ if service in default_service_protocol.keys():
+ srv['protocol'] = default_service_protocol[service]
+ else:
+ srv['custom'] = True
+
+ if conf.exists(['login']):
+ srv['login'] = conf.return_value(['login'])
+
+ if conf.exists(['host-name']):
+ srv['host'] = conf.return_values(['host-name'])
+
+ if conf.exists(['protocol']):
+ srv['protocol'] = conf.return_value(['protocol'])
+
+ if conf.exists(['password']):
+ srv['password'] = conf.return_value(['password'])
+
+ if conf.exists(['server']):
+ srv['server'] = conf.return_value(['server'])
+
+ if conf.exists(['zone']):
+ srv['zone'] = conf.return_value(['zone'])
+ elif srv['provider'] == 'cloudflare':
+ # default populate zone entry with bar.tld if
+ # host-name is foo.bar.tld
+ srv['zone'] = srv['host'][0].split('.',1)[1]
+
+ node['service'].append(srv)
+
+ # Set config back to appropriate level for these options
+ conf.set_level(base_level + ['interface', interface])
+
+ # Additional settings in CLI
+ if conf.exists(['use-web', 'skip']):
+ node['web_skip'] = conf.return_value(['use-web', 'skip'])
+
+ if conf.exists(['use-web', 'url']):
+ node['web_url'] = conf.return_value(['use-web', 'url'])
+
+ # set config level back to top level
+ conf.set_level(base_level)
+
+ dyndns['interfaces'].append(node)
+
+ return dyndns
+
+def verify(dyndns):
+ # bail out early - looks like removal from running config
+ if dyndns['deleted']:
+ return None
+
+ # A 'node' corresponds to an interface
+ for node in dyndns['interfaces']:
+
+ # RFC2136 - configuration validation
+ for rfc2136 in node['rfc2136']:
+ if not rfc2136['record']:
+ raise ConfigError('Set key for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+
+ if not rfc2136['zone']:
+ raise ConfigError('Set zone for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+
+ if not rfc2136['keyfile']:
+ raise ConfigError('Set keyfile for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+ else:
+ if not os.path.isfile(rfc2136['keyfile']):
+ raise ConfigError('Keyfile for service "{0}" to send DDNS updates for interface "{1}" does not exist'.format(rfc2136['name'], node['interface']))
+
+ if not rfc2136['server']:
+ raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface']))
+
+ # DynDNS service provider - configuration validation
+ for service in node['service']:
+ if not service['host']:
+ raise ConfigError('Set host-name for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if not service['login']:
+ raise ConfigError('Set login for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if not service['password']:
+ raise ConfigError('Set password for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if service['custom'] is True:
+ if not service['protocol']:
+ raise ConfigError('Set protocol for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if not service['server']:
+ raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface']))
+
+ if service['zone']:
+ if service['provider'] != 'cloudflare':
+ raise ConfigError('Zone option not allowed for "{0}", it can only be used for CloudFlare'.format(service['provider']))
+
+ return None
+
+def generate(dyndns):
+ # bail out early - looks like removal from running config
+ if dyndns['deleted']:
+ return None
+
+ render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns)
+
+ # Config file must be accessible only by its owner
+ os.chmod(config_file, S_IRUSR | S_IWUSR)
+
+ return None
+
+def apply(dyndns):
+ if dyndns['deleted']:
+ 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/firewall_options.py b/src/conf_mode/firewall_options.py
new file mode 100755
index 000000000..71b2a98b3
--- /dev/null
+++ b/src/conf_mode/firewall_options.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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
+import os
+import copy
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'intf_opts': [],
+ 'new_chain4': False,
+ 'new_chain6': False
+}
+
+def get_config():
+ opts = copy.deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists('firewall options'):
+ # bail out early
+ return opts
+ else:
+ conf.set_level('firewall options')
+
+ # Parse configuration of each individual instance
+ if conf.exists('interface'):
+ for intf in conf.list_nodes('interface'):
+ conf.set_level('firewall options interface {0}'.format(intf))
+ config = {
+ 'intf': intf,
+ 'disabled': False,
+ 'mss4': '',
+ 'mss6': ''
+ }
+
+ # Check if individual option is disabled
+ if conf.exists('disable'):
+ config['disabled'] = True
+
+ #
+ # Get MSS value IPv4
+ #
+ if conf.exists('adjust-mss'):
+ config['mss4'] = conf.return_value('adjust-mss')
+
+ # We need a marker that a new iptables chain needs to be generated
+ if not opts['new_chain4']:
+ opts['new_chain4'] = True
+
+ #
+ # Get MSS value IPv6
+ #
+ if conf.exists('adjust-mss6'):
+ config['mss6'] = conf.return_value('adjust-mss6')
+
+ # We need a marker that a new ip6tables chain needs to be generated
+ if not opts['new_chain6']:
+ opts['new_chain6'] = True
+
+ # Append interface options to global list
+ opts['intf_opts'].append(config)
+
+ return opts
+
+def verify(tcp):
+ # syntax verification is done via cli
+ return None
+
+def apply(tcp):
+ target = 'VYOS_FW_OPTIONS'
+
+ # always cleanup iptables
+ call('iptables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
+ call('iptables --table mangle --flush {} >&/dev/null'.format(target))
+ call('iptables --table mangle --delete-chain {} >&/dev/null'.format(target))
+
+ # always cleanup ip6tables
+ call('ip6tables --table mangle --delete FORWARD --jump {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --flush {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --delete-chain {} >&/dev/null'.format(target))
+
+ # Setup new iptables rules
+ if tcp['new_chain4']:
+ call('iptables --table mangle --new-chain {} >&/dev/null'.format(target))
+ call('iptables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
+
+ for opts in tcp['intf_opts']:
+ intf = opts['intf']
+ mss = opts['mss4']
+
+ # Check if this rule iis disabled
+ if opts['disabled']:
+ continue
+
+ # adjust TCP MSS per interface
+ if mss:
+ call('iptables --table mangle --append {} --out-interface {} --protocol tcp '
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
+
+ # Setup new ip6tables rules
+ if tcp['new_chain6']:
+ call('ip6tables --table mangle --new-chain {} >&/dev/null'.format(target))
+ call('ip6tables --table mangle --append FORWARD --jump {} >&/dev/null'.format(target))
+
+ for opts in tcp['intf_opts']:
+ intf = opts['intf']
+ mss = opts['mss6']
+
+ # Check if this rule iis disabled
+ if opts['disabled']:
+ continue
+
+ # adjust TCP MSS per interface
+ if mss:
+ call('ip6tables --table mangle --append {} --out-interface {} --protocol tcp '
+ '--tcp-flags SYN,RST SYN --jump TCPMSS --set-mss {} >&/dev/null'.format(target, intf, mss))
+
+ return None
+
+if __name__ == '__main__':
+
+ try:
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py
new file mode 100755
index 000000000..b7e73eaeb
--- /dev/null
+++ b/src/conf_mode/flow_accounting_conf.py
@@ -0,0 +1,371 @@
+#!/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
+import ipaddress
+
+from ipaddress import ip_address
+from jinja2 import FileSystemLoader, Environment
+
+from vyos.ifconfig import Section
+from vyos.ifconfig import Interface
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+# default values
+default_sflow_server_port = 6343
+default_netflow_server_port = 2055
+default_plugin_pipe_size = 10
+default_captured_packet_size = 128
+default_netflow_version = '9'
+default_sflow_agentip = 'auto'
+uacctd_conf_path = '/etc/pmacct/uacctd.conf'
+iptables_nflog_table = 'raw'
+iptables_nflog_chain = 'VYATTA_CT_PREROUTING_HOOK'
+
+# helper functions
+# check if node exists and return True if this is true
+def _node_exists(path):
+ vyos_config = Config()
+ if vyos_config.exists(path):
+ return True
+
+# get sFlow agent-ip if agent-address is "auto" (default behaviour)
+def _sflow_default_agentip(config):
+ # check if any of BGP, OSPF, OSPFv3 protocols are configured and use router-id from there
+ if config.exists('protocols bgp'):
+ bgp_router_id = config.return_value("protocols bgp {} parameters router-id".format(config.list_nodes('protocols bgp')[0]))
+ if bgp_router_id:
+ return bgp_router_id
+ if config.return_value('protocols ospf parameters router-id'):
+ return config.return_value('protocols ospf parameters router-id')
+ if config.return_value('protocols ospfv3 parameters router-id'):
+ return config.return_value('protocols ospfv3 parameters router-id')
+
+ # if router-id was not found, use first available ip of any interface
+ for iface in Section.interfaces():
+ for address in Interface(iface).get_addr():
+ # return an IP, if this is not loopback
+ regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
+ if regex_filter.search(address):
+ return regex_filter.search(address).group('ipaddr')
+
+ # return nothing by default
+ return None
+
+# get iptables rule dict for chain in table
+def _iptables_get_nflog():
+ # define list with rules
+ rules = []
+
+ # prepare regex for parsing rules
+ rule_pattern = "^-A (?P<rule_definition>{0} -i (?P<interface>[\w\.\*\-]+).*--comment FLOW_ACCOUNTING_RULE.* -j NFLOG.*$)".format(iptables_nflog_chain)
+ rule_re = re.compile(rule_pattern)
+
+ for iptables_variant in ['iptables', 'ip6tables']:
+ # run iptables, save output and split it by lines
+ iptables_command = f'{iptables_variant} -t {iptables_nflog_table} -S {iptables_nflog_chain}'
+ tmp = cmd(iptables_command, message='Failed to get flows list')
+
+ # parse each line and add information to list
+ for current_rule in tmp.splitlines():
+ current_rule_parsed = rule_re.search(current_rule)
+ if current_rule_parsed:
+ rules.append({ 'interface': current_rule_parsed.groupdict()["interface"], 'iptables_variant': iptables_variant, 'table': iptables_nflog_table, 'rule_definition': current_rule_parsed.groupdict()["rule_definition"] })
+
+ # return list with rules
+ return rules
+
+# modify iptables rules
+def _iptables_config(configured_ifaces):
+ # define list of iptables commands to modify settings
+ iptable_commands = []
+
+ # prepare extended list with configured interfaces
+ configured_ifaces_extended = []
+ for iface in configured_ifaces:
+ configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'iptables' })
+ configured_ifaces_extended.append({ 'iface': iface, 'iptables_variant': 'ip6tables' })
+
+ # get currently configured interfaces with iptables rules
+ active_nflog_rules = _iptables_get_nflog()
+
+ # compare current active list with configured one and delete excessive interfaces, add missed
+ active_nflog_ifaces = []
+ for rule in active_nflog_rules:
+ iptables = rule['iptables_variant']
+ interface = rule['interface']
+ if interface not in configured_ifaces:
+ table = rule['table']
+ rule = rule['rule_definition']
+ iptable_commands.append(f'{iptables} -t {table} -D {rule}')
+ else:
+ active_nflog_ifaces.append({
+ 'iface': interface,
+ 'iptables_variant': iptables,
+ })
+
+ # do not create new rules for already configured interfaces
+ for iface in active_nflog_ifaces:
+ if iface in active_nflog_ifaces:
+ configured_ifaces_extended.remove(iface)
+
+ # create missed rules
+ for iface_extended in configured_ifaces_extended:
+ iface = iface_extended['iface']
+ iptables = iface_extended['iptables_variant']
+ rule_definition = f'{iptables_nflog_chain} -i {iface} -m comment --comment FLOW_ACCOUNTING_RULE -j NFLOG --nflog-group 2 --nflog-size {default_captured_packet_size} --nflog-threshold 100'
+ iptable_commands.append(f'{iptables} -t {iptables_nflog_table} -I {rule_definition}')
+
+ # change iptables
+ for command in iptable_commands:
+ cmd(command, raising=ConfigError)
+
+
+def get_config():
+ vc = Config()
+ vc.set_level('')
+ # Convert the VyOS config to an abstract internal representation
+ flow_config = {
+ 'flow-accounting-configured': vc.exists('system flow-accounting'),
+ 'buffer-size': vc.return_value('system flow-accounting buffer-size'),
+ 'disable-imt': _node_exists('system flow-accounting disable-imt'),
+ 'syslog-facility': vc.return_value('system flow-accounting syslog-facility'),
+ 'interfaces': None,
+ 'sflow': {
+ 'configured': vc.exists('system flow-accounting sflow'),
+ 'agent-address': vc.return_value('system flow-accounting sflow agent-address'),
+ 'sampling-rate': vc.return_value('system flow-accounting sflow sampling-rate'),
+ 'servers': None
+ },
+ 'netflow': {
+ 'configured': vc.exists('system flow-accounting netflow'),
+ 'engine-id': vc.return_value('system flow-accounting netflow engine-id'),
+ 'max-flows': vc.return_value('system flow-accounting netflow max-flows'),
+ 'sampling-rate': vc.return_value('system flow-accounting netflow sampling-rate'),
+ 'source-ip': vc.return_value('system flow-accounting netflow source-ip'),
+ 'version': vc.return_value('system flow-accounting netflow version'),
+ 'timeout': {
+ 'expint': vc.return_value('system flow-accounting netflow timeout expiry-interval'),
+ 'general': vc.return_value('system flow-accounting netflow timeout flow-generic'),
+ 'icmp': vc.return_value('system flow-accounting netflow timeout icmp'),
+ 'maxlife': vc.return_value('system flow-accounting netflow timeout max-active-life'),
+ 'tcp.fin': vc.return_value('system flow-accounting netflow timeout tcp-fin'),
+ 'tcp': vc.return_value('system flow-accounting netflow timeout tcp-generic'),
+ 'tcp.rst': vc.return_value('system flow-accounting netflow timeout tcp-rst'),
+ 'udp': vc.return_value('system flow-accounting netflow timeout udp')
+ },
+ 'servers': None
+ }
+ }
+
+ # get interfaces list
+ if vc.exists('system flow-accounting interface'):
+ flow_config['interfaces'] = vc.return_values('system flow-accounting interface')
+
+ # get sFlow collectors list
+ if vc.exists('system flow-accounting sflow server'):
+ flow_config['sflow']['servers'] = []
+ sflow_collectors = vc.list_nodes('system flow-accounting sflow server')
+ for collector in sflow_collectors:
+ port = default_sflow_server_port
+ if vc.return_value("system flow-accounting sflow server {} port".format(collector)):
+ port = vc.return_value("system flow-accounting sflow server {} port".format(collector))
+ flow_config['sflow']['servers'].append({ 'address': collector, 'port': port })
+
+ # get NetFlow collectors list
+ if vc.exists('system flow-accounting netflow server'):
+ flow_config['netflow']['servers'] = []
+ netflow_collectors = vc.list_nodes('system flow-accounting netflow server')
+ for collector in netflow_collectors:
+ port = default_netflow_server_port
+ if vc.return_value("system flow-accounting netflow server {} port".format(collector)):
+ port = vc.return_value("system flow-accounting netflow server {} port".format(collector))
+ flow_config['netflow']['servers'].append({ 'address': collector, 'port': port })
+
+ # get sflow agent-id
+ if flow_config['sflow']['agent-address'] == None or flow_config['sflow']['agent-address'] == 'auto':
+ flow_config['sflow']['agent-address'] = _sflow_default_agentip(vc)
+
+ # get NetFlow version
+ if not flow_config['netflow']['version']:
+ flow_config['netflow']['version'] = default_netflow_version
+
+ # convert NetFlow engine-id format, if this is necessary
+ if flow_config['netflow']['engine-id'] and flow_config['netflow']['version'] == '5':
+ regex_filter = re.compile('^\d+$')
+ if regex_filter.search(flow_config['netflow']['engine-id']):
+ flow_config['netflow']['engine-id'] = "{}:0".format(flow_config['netflow']['engine-id'])
+
+ # return dict with flow-accounting configuration
+ return flow_config
+
+def verify(config):
+ # Verify that configuration is valid
+ # skip all checks if flow-accounting was removed
+ if not config['flow-accounting-configured']:
+ return True
+
+ # check if at least one collector is enabled
+ if not (config['sflow']['configured'] or config['netflow']['configured'] or not config['disable-imt']):
+ raise ConfigError("You need to configure at least one sFlow or NetFlow protocol, or not set \"disable-imt\" for flow-accounting")
+
+ # Check if at least one interface is configured
+ if not config['interfaces']:
+ raise ConfigError("You need to configure at least one interface for flow-accounting")
+
+ # check that all configured interfaces exists in the system
+ for iface in config['interfaces']:
+ if not iface in Section.interfaces():
+ # chnged from error to warning to allow adding dynamic interfaces and interface templates
+ # raise ConfigError("The {} interface is not presented in the system".format(iface))
+ print("Warning: the {} interface is not presented in the system".format(iface))
+
+ # check sFlow configuration
+ if config['sflow']['configured']:
+ # check if at least one sFlow collector is configured if sFlow configuration is presented
+ if not config['sflow']['servers']:
+ raise ConfigError("You need to configure at least one sFlow server")
+
+ # check that all sFlow collectors use the same IP protocol version
+ sflow_collector_ipver = None
+ for sflow_collector in config['sflow']['servers']:
+ if sflow_collector_ipver:
+ if sflow_collector_ipver != ip_address(sflow_collector['address']).version:
+ raise ConfigError("All sFlow servers must use the same IP protocol")
+ else:
+ sflow_collector_ipver = ip_address(sflow_collector['address']).version
+
+
+ # check agent-id for sFlow: we should avoid mixing IPv4 agent-id with IPv6 collectors and vice-versa
+ for sflow_collector in config['sflow']['servers']:
+ if ip_address(sflow_collector['address']).version != ip_address(config['sflow']['agent-address']).version:
+ raise ConfigError("Different IP address versions cannot be mixed in \"sflow agent-address\" and \"sflow server\". You need to set manually the same IP version for \"agent-address\" as for all sFlow servers")
+
+ # check if configured sFlow agent-id exist in the system
+ agent_id_presented = None
+ for iface in Section.interfaces():
+ for address in Interface(iface).get_addr():
+ # check an IP, if this is not loopback
+ regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
+ if regex_filter.search(address):
+ if regex_filter.search(address).group('ipaddr') == config['sflow']['agent-address']:
+ agent_id_presented = True
+ break
+ if not agent_id_presented:
+ raise ConfigError("Your \"sflow agent-address\" does not exist in the system")
+
+ # check NetFlow configuration
+ if config['netflow']['configured']:
+ # check if at least one NetFlow collector is configured if NetFlow configuration is presented
+ if not config['netflow']['servers']:
+ raise ConfigError("You need to configure at least one NetFlow server")
+
+ # check if configured netflow source-ip exist in the system
+ if config['netflow']['source-ip']:
+ source_ip_presented = None
+ for iface in Section.interfaces():
+ for address in Interface(iface).get_addr():
+ # check an IP
+ regex_filter = re.compile('^(?!(127)|(::1)|(fe80))(?P<ipaddr>[a-f\d\.:]+)/\d+$')
+ if regex_filter.search(address):
+ if regex_filter.search(address).group('ipaddr') == config['netflow']['source-ip']:
+ source_ip_presented = True
+ break
+ if not source_ip_presented:
+ raise ConfigError("Your \"netflow source-ip\" does not exist in the system")
+
+ # check if engine-id compatible with selected protocol version
+ if config['netflow']['engine-id']:
+ v5_filter = '^(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5]):(\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$'
+ v9v10_filter = '^(\d|[1-9]\d{1,8}|[1-3]\d{9}|4[01]\d{8}|42[0-8]\d{7}|429[0-3]\d{6}|4294[0-8]\d{5}|42949[0-5]\d{4}|429496[0-6]\d{3}|4294967[01]\d{2}|42949672[0-8]\d|429496729[0-5])$'
+ if config['netflow']['version'] == '5':
+ regex_filter = re.compile(v5_filter)
+ if not regex_filter.search(config['netflow']['engine-id']):
+ raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version']))
+ else:
+ regex_filter = re.compile(v9v10_filter)
+ if not regex_filter.search(config['netflow']['engine-id']):
+ raise ConfigError("You cannot use NetFlow engine-id {} together with NetFlow protocol version {}".format(config['netflow']['engine-id'], config['netflow']['version']))
+
+ # return True if all checks were passed
+ return True
+
+def generate(config):
+ # skip all checks if flow-accounting was removed
+ if not config['flow-accounting-configured']:
+ return True
+
+ # Calculate all necessary values
+ if config['buffer-size']:
+ # circular queue size
+ config['plugin_pipe_size'] = int(config['buffer-size']) * 1024**2
+ else:
+ config['plugin_pipe_size'] = default_plugin_pipe_size * 1024**2
+ # transfer buffer size
+ # recommended value from pmacct developers 1/1000 of pipe size
+ config['plugin_buffer_size'] = int(config['plugin_pipe_size'] / 1000)
+
+ # Prepare a timeouts string
+ timeout_string = ''
+ for timeout_type, timeout_value in config['netflow']['timeout'].items():
+ if timeout_value:
+ if timeout_string == '':
+ timeout_string = "{}{}={}".format(timeout_string, timeout_type, timeout_value)
+ else:
+ timeout_string = "{}:{}={}".format(timeout_string, timeout_type, timeout_value)
+ config['netflow']['timeout_string'] = timeout_string
+
+ render(uacctd_conf_path, 'netflow/uacctd.conf.tmpl', {
+ 'templatecfg': config,
+ 'snaplen': default_captured_packet_size,
+ })
+
+
+def apply(config):
+ # define variables
+ command = None
+ # Check if flow-accounting was removed and define command
+ if not config['flow-accounting-configured']:
+ command = 'systemctl stop uacctd.service'
+ else:
+ command = 'systemctl restart uacctd.service'
+
+ # run command to start or stop flow-accounting
+ cmd(command, raising=ConfigError, message='Failed to start/stop flow-accounting')
+
+ # configure iptables rules for defined interfaces
+ if config['interfaces']:
+ _iptables_config(config['interfaces'])
+ else:
+ _iptables_config([])
+
+if __name__ == '__main__':
+ try:
+ config = get_config()
+ verify(config)
+ generate(config)
+ apply(config)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/host_name.py b/src/conf_mode/host_name.py
new file mode 100755
index 000000000..9d66bd434
--- /dev/null
+++ b/src/conf_mode/host_name.py
@@ -0,0 +1,175 @@
+#!/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/>.
+
+"""
+conf-mode script for 'system host-name' and 'system domain-name'.
+"""
+
+import re
+import sys
+import copy
+
+import vyos.util
+import vyos.hostsd_client
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd, call, process_named_running
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'hostname': 'vyos',
+ 'domain_name': '',
+ 'domain_search': [],
+ 'nameserver': [],
+ 'nameservers_dhcp_interfaces': [],
+ 'static_host_mapping': {}
+}
+
+hostsd_tag = 'system'
+
+def get_config():
+ conf = Config()
+
+ hosts = copy.deepcopy(default_config_data)
+
+ hosts['hostname'] = conf.return_value("system host-name")
+
+ # This may happen if the config is not loaded yet,
+ # e.g. if run by cloud-init
+ if not hosts['hostname']:
+ hosts['hostname'] = default_config_data['hostname']
+
+ if conf.exists("system domain-name"):
+ hosts['domain_name'] = conf.return_value("system domain-name")
+ hosts['domain_search'].append(hosts['domain_name'])
+
+ for search in conf.return_values("system domain-search domain"):
+ hosts['domain_search'].append(search)
+
+ hosts['nameserver'] = conf.return_values("system name-server")
+
+ hosts['nameservers_dhcp_interfaces'] = conf.return_values("system name-servers-dhcp")
+
+ # system static-host-mapping
+ for hn in conf.list_nodes('system static-host-mapping host-name'):
+ hosts['static_host_mapping'][hn] = {}
+ hosts['static_host_mapping'][hn]['address'] = conf.return_value(f'system static-host-mapping host-name {hn} inet')
+ hosts['static_host_mapping'][hn]['aliases'] = conf.return_values(f'system static-host-mapping host-name {hn} alias')
+
+ return hosts
+
+
+def verify(hosts):
+ if hosts is None:
+ return None
+
+ # pattern $VAR(@) "^[[:alnum:]][-.[:alnum:]]*[[:alnum:]]$" ; "invalid host name $VAR(@)"
+ hostname_regex = re.compile("^[A-Za-z0-9][-.A-Za-z0-9]*[A-Za-z0-9]$")
+ if not hostname_regex.match(hosts['hostname']):
+ raise ConfigError('Invalid host name ' + hosts["hostname"])
+
+ # pattern $VAR(@) "^.{1,63}$" ; "invalid host-name length"
+ length = len(hosts['hostname'])
+ if length < 1 or length > 63:
+ raise ConfigError(
+ 'Invalid host-name length, must be less than 63 characters')
+
+ all_static_host_mapping_addresses = []
+ # static mappings alias hostname
+ for host, hostprops in hosts['static_host_mapping'].items():
+ if not hostprops['address']:
+ raise ConfigError(f'IP address required for static-host-mapping "{host}"')
+ all_static_host_mapping_addresses.append(hostprops['address'])
+ for a in hostprops['aliases']:
+ if not hostname_regex.match(a) and len(a) != 0:
+ raise ConfigError(f'Invalid alias "{a}" in static-host-mapping "{host}"')
+
+ # TODO: add warnings for nameservers_dhcp_interfaces if interface doesn't
+ # exist or doesn't have address dhcp(v6)
+
+ return None
+
+
+def generate(config):
+ pass
+
+def apply(config):
+ if config is None:
+ return None
+
+ ## Send the updated data to vyos-hostsd
+ try:
+ hc = vyos.hostsd_client.Client()
+
+ hc.set_host_name(config['hostname'], config['domain_name'])
+
+ hc.delete_search_domains([hostsd_tag])
+ if config['domain_search']:
+ hc.add_search_domains({hostsd_tag: config['domain_search']})
+
+ hc.delete_name_servers([hostsd_tag])
+ if config['nameserver']:
+ hc.add_name_servers({hostsd_tag: config['nameserver']})
+
+ # add our own tag's (system) nameservers and search to resolv.conf
+ hc.delete_name_server_tags_system(hc.get_name_server_tags_system())
+ hc.add_name_server_tags_system([hostsd_tag])
+
+ # this will add the dhcp client nameservers to resolv.conf
+ for intf in config['nameservers_dhcp_interfaces']:
+ hc.add_name_server_tags_system([f'dhcp-{intf}', f'dhcpv6-{intf}'])
+
+ hc.delete_hosts([hostsd_tag])
+ if config['static_host_mapping']:
+ hc.add_hosts({hostsd_tag: config['static_host_mapping']})
+
+ hc.apply()
+ except vyos.hostsd_client.VyOSHostsdError as e:
+ raise ConfigError(str(e))
+
+ ## Actually update the hostname -- vyos-hostsd doesn't do that
+
+ # No domain name -- the Debian way.
+ hostname_new = config['hostname']
+
+ # rsyslog runs into a race condition at boot time with systemd
+ # restart rsyslog only if the hostname changed.
+ hostname_old = cmd('hostnamectl --static')
+ call(f'hostnamectl set-hostname --static {hostname_new}')
+
+ # Restart services that use the hostname
+ if hostname_new != hostname_old:
+ call("systemctl restart rsyslog.service")
+
+ # If SNMP is running, restart it too
+ if process_named_running('snmpd'):
+ call('systemctl restart snmpd.service')
+
+ return None
+
+
+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/http-api.py b/src/conf_mode/http-api.py
new file mode 100755
index 000000000..b8a084a40
--- /dev/null
+++ b/src/conf_mode/http-api.py
@@ -0,0 +1,113 @@
+#!/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 sys
+import os
+import json
+from copy import deepcopy
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+config_file = '/etc/vyos/http-api.conf'
+
+vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def get_config():
+ http_api = deepcopy(vyos.defaults.api_data)
+ x = http_api.get('api_keys')
+ if x is None:
+ default_key = None
+ else:
+ default_key = x[0]
+ keys_added = False
+
+ conf = Config()
+ if not conf.exists('service https api'):
+ return None
+ else:
+ conf.set_level('service https api')
+
+ if conf.exists('strict'):
+ http_api['strict'] = 'true'
+
+ if conf.exists('debug'):
+ http_api['debug'] = 'true'
+
+ if conf.exists('port'):
+ port = conf.return_value('port')
+ http_api['port'] = port
+
+ if conf.exists('keys'):
+ for name in conf.list_nodes('keys id'):
+ if conf.exists('keys id {0} key'.format(name)):
+ key = conf.return_value('keys id {0} key'.format(name))
+ new_key = { 'id': name, 'key': key }
+ http_api['api_keys'].append(new_key)
+ keys_added = True
+
+ if keys_added and default_key:
+ if default_key in http_api['api_keys']:
+ http_api['api_keys'].remove(default_key)
+
+ return http_api
+
+def verify(http_api):
+ return None
+
+def generate(http_api):
+ if http_api is None:
+ return None
+
+ if not os.path.exists('/etc/vyos'):
+ os.mkdir('/etc/vyos')
+
+ with open(config_file, 'w') as f:
+ json.dump(http_api, f, indent=2)
+
+ return None
+
+def apply(http_api):
+ if http_api is not None:
+ call('systemctl restart vyos-http-api.service')
+ else:
+ call('systemctl stop vyos-http-api.service')
+
+ for dep in dependencies:
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+
+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/https.py b/src/conf_mode/https.py
new file mode 100755
index 000000000..a13f131ab
--- /dev/null
+++ b/src/conf_mode/https.py
@@ -0,0 +1,185 @@
+#!/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():
+ 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', ['_'])
+ # XXX: T2636 workaround: convert string to a list with one element
+ if not isinstance(name, list):
+ name = [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:
+ # XXX: T2636 workaround: convert string to a list with one element
+ if not isinstance(cert_domains, list):
+ cert_domains = [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', [])
+ # XXX: T2636 workaround: convert string to a list with one element
+ if not isinstance(vhosts, list):
+ vhosts = [vhosts]
+ 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)
+
+ 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
new file mode 100755
index 000000000..49aea9b7f
--- /dev/null
+++ b/src/conf_mode/igmp_proxy.py
@@ -0,0 +1,142 @@
+#!/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 copy import deepcopy
+
+from netifaces import interfaces
+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/igmpproxy.conf'
+
+default_config_data = {
+ 'disable': False,
+ 'disable_quickleave': False,
+ 'interfaces': [],
+}
+
+def get_config():
+ igmp_proxy = deepcopy(default_config_data)
+ conf = Config()
+ base = ['protocols', 'igmp-proxy']
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ # Network interfaces to listen on
+ if conf.exists(['disable']):
+ igmp_proxy['disable'] = True
+
+ # Option to disable "quickleave"
+ if conf.exists(['disable-quickleave']):
+ igmp_proxy['disable_quickleave'] = True
+
+ for intf in conf.list_nodes(['interface']):
+ conf.set_level(base + ['interface', intf])
+ interface = {
+ 'name': intf,
+ 'alt_subnet': [],
+ 'role': 'downstream',
+ 'threshold': '1',
+ 'whitelist': []
+ }
+
+ if conf.exists(['alt-subnet']):
+ interface['alt_subnet'] = conf.return_values(['alt-subnet'])
+
+ if conf.exists(['role']):
+ interface['role'] = conf.return_value(['role'])
+
+ if conf.exists(['threshold']):
+ interface['threshold'] = conf.return_value(['threshold'])
+
+ if conf.exists(['whitelist']):
+ interface['whitelist'] = conf.return_values(['whitelist'])
+
+ # Append interface configuration to global configuration list
+ igmp_proxy['interfaces'].append(interface)
+
+ return igmp_proxy
+
+def verify(igmp_proxy):
+ # bail out early - looks like removal from running config
+ if igmp_proxy is None:
+ return None
+
+ # bail out early - service is disabled
+ if igmp_proxy['disable']:
+ return None
+
+ # at least two interfaces are required, one upstream and one downstream
+ if len(igmp_proxy['interfaces']) < 2:
+ raise ConfigError('Must define an upstream and at least 1 downstream interface!')
+
+ upstream = 0
+ for interface in igmp_proxy['interfaces']:
+ if interface['name'] not in interfaces():
+ raise ConfigError('Interface "{}" does not exist'.format(interface['name']))
+ if "upstream" == interface['role']:
+ 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 igmp_proxy is None:
+ return None
+
+ # bail out early - service is disabled, but inform user
+ if igmp_proxy['disable']:
+ print('Warning: IGMP Proxy will be deactivated because it is disabled')
+ return None
+
+ render(config_file, 'igmp-proxy/igmpproxy.conf.tmpl', igmp_proxy)
+ return None
+
+def apply(igmp_proxy):
+ if igmp_proxy is None or igmp_proxy['disable']:
+ # 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/intel_qat.py b/src/conf_mode/intel_qat.py
new file mode 100755
index 000000000..742f09a54
--- /dev/null
+++ b/src/conf_mode/intel_qat.py
@@ -0,0 +1,103 @@
+#!/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 sys
+import os
+import re
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import popen, run
+
+from vyos import airbag
+airbag.enable()
+
+# Define for recovering
+gl_ipsec_conf = None
+
+def get_config():
+ c = Config()
+ config_data = {
+ 'qat_conf' : None,
+ 'ipsec_conf' : None,
+ 'openvpn_conf' : None,
+ }
+
+ if c.exists('system acceleration qat'):
+ config_data['qat_conf'] = True
+
+ if c.exists('vpn ipsec '):
+ gl_ipsec_conf = True
+ config_data['ipsec_conf'] = True
+
+ if c.exists('interfaces openvpn'):
+ config_data['openvpn_conf'] = True
+
+ return config_data
+
+# Control configured VPN service which can use QAT
+def vpn_control(action):
+ # XXX: Should these commands report failure
+ if action == 'restore' and gl_ipsec_conf:
+ return run('ipsec start')
+ return run(f'ipsec {action}')
+
+def verify(c):
+ # Check if QAT service installed
+ if not os.path.exists('/etc/init.d/qat_service'):
+ raise ConfigError("Warning: QAT init file not found")
+
+ if c['qat_conf'] == None:
+ return
+
+ # Check if QAT device exist
+ output, err = popen('lspci -nn', decode='utf-8')
+ if not err:
+ data = re.findall('(8086:19e2)|(8086:37c8)|(8086:0435)|(8086:6f54)', output)
+ #If QAT devices found
+ if not data:
+ print("\t No QAT acceleration device found")
+ sys.exit(1)
+
+def apply(c):
+ if c['ipsec_conf']:
+ # Shutdown VPN service which can use QAT
+ vpn_control('stop')
+
+ # Disable QAT service
+ if c['qat_conf'] == None:
+ run('/etc/init.d/qat_service stop')
+ if c['ipsec_conf']:
+ vpn_control('start')
+ return
+
+ # Run qat init.d script
+ run('/etc/init.d/qat_service start')
+ if c['ipsec_conf']:
+ # Recovery VPN service
+ vpn_control('start')
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ vpn_control('restore')
+ sys.exit(1)
diff --git a/src/conf_mode/interfaces-bonding.py b/src/conf_mode/interfaces-bonding.py
new file mode 100755
index 000000000..3b238f1ea
--- /dev/null
+++ b/src/conf_mode/interfaces-bonding.py
@@ -0,0 +1,197 @@
+#!/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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+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 BondIf
+from vyos.validate import is_member
+from vyos.validate import has_address_configured
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_bond_mode(mode):
+ if mode == 'round-robin':
+ return 'balance-rr'
+ elif mode == 'active-backup':
+ return 'active-backup'
+ elif mode == 'xor-hash':
+ return 'balance-xor'
+ elif mode == 'broadcast':
+ return 'broadcast'
+ elif mode == '802.3ad':
+ return '802.3ad'
+ elif mode == 'transmit-load-balance':
+ return 'balance-tlb'
+ elif mode == 'adaptive-load-balance':
+ return 'balance-alb'
+ else:
+ raise ConfigError(f'invalid bond mode "{mode}"')
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'bonding']
+ bond = get_interface_dict(conf, base)
+
+ # To make our own life easier transfor the list of member interfaces
+ # into a dictionary - we will use this to add additional information
+ # later on for wach member
+ if 'member' in bond and 'interface' in bond['member']:
+ # first convert it to a list if only one member is given
+ if isinstance(bond['member']['interface'], str):
+ bond['member']['interface'] = [bond['member']['interface']]
+
+ tmp={}
+ for interface in bond['member']['interface']:
+ tmp.update({interface: {}})
+
+ bond['member']['interface'] = tmp
+
+ if 'mode' in bond:
+ bond['mode'] = get_bond_mode(bond['mode'])
+
+ tmp = leaf_node_changed(conf, ['mode'])
+ if tmp:
+ bond.update({'shutdown_required': ''})
+
+ # determine which members have been removed
+ tmp = leaf_node_changed(conf, ['member', 'interface'])
+ if tmp:
+ bond.update({'shutdown_required': ''})
+ if 'member' in bond:
+ bond['member'].update({'interface_remove': tmp })
+ else:
+ bond.update({'member': {'interface_remove': tmp }})
+
+ if 'member' in bond and 'interface' in bond['member']:
+ for interface, interface_config in bond['member']['interface'].items():
+ # Check if we are a member of another bond device
+ tmp = is_member(conf, interface, 'bridge')
+ if tmp:
+ interface_config.update({'is_bridge_member' : tmp})
+
+ # Check if we are a member of a bond device
+ tmp = is_member(conf, interface, 'bonding')
+ if tmp and tmp != bond['ifname']:
+ interface_config.update({'is_bond_member' : tmp})
+
+ # bond members must not have an assigned address
+ tmp = has_address_configured(conf, interface)
+ if tmp:
+ interface_config.update({'has_address' : ''})
+
+ return bond
+
+
+def verify(bond):
+ if 'deleted' in bond:
+ verify_bridge_delete(bond)
+ return None
+
+ if 'arp_monitor' in bond:
+ if 'target' in bond['arp_monitor'] and len(int(bond['arp_monitor']['target'])) > 16:
+ raise ConfigError('The maximum number of arp-monitor targets is 16')
+
+ if 'interval' in bond['arp_monitor'] and len(int(bond['arp_monitor']['interval'])) > 0:
+ if bond['mode'] in ['802.3ad', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('ARP link monitoring does not work for mode 802.3ad, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ if 'primary' in bond:
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('Option primary - mode dependency failed, not'
+ 'supported in mode {mode}!'.format(**bond))
+
+ verify_address(bond)
+ verify_dhcpv6(bond)
+ verify_vrf(bond)
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(bond)
+
+ bond_name = bond['ifname']
+ if 'member' in bond:
+ member = bond.get('member')
+ for interface, interface_config in member.get('interface', {}).items():
+ error_msg = f'Can not add interface "{interface}" to bond "{bond_name}", '
+
+ if interface == 'lo':
+ raise ConfigError('Loopback interface "lo" can not be added to a bond')
+
+ if interface not in interfaces():
+ raise ConfigError(error_msg + 'it does not exist!')
+
+ if 'is_bridge_member' in interface_config:
+ tmp = interface_config['is_bridge_member']
+ raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
+
+ if 'is_bond_member' in interface_config:
+ tmp = interface_config['is_bond_member']
+ raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
+
+ if 'has_address' in interface_config:
+ raise ConfigError(error_msg + 'it has an address assigned!')
+
+
+ if 'primary' in bond:
+ if bond['primary'] not in bond['member']['interface']:
+ raise ConfigError(f'Primary interface of bond "{bond_name}" must be a member interface')
+
+ if bond['mode'] not in ['active-backup', 'balance-tlb', 'balance-alb']:
+ raise ConfigError('primary interface only works for mode active-backup, ' \
+ 'transmit-load-balance or adaptive-load-balance')
+
+ return None
+
+def generate(bond):
+ return None
+
+def apply(bond):
+ b = BondIf(bond['ifname'])
+
+ if 'deleted' in bond:
+ # delete interface
+ b.remove()
+ else:
+ b.update(bond)
+
+ 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-bridge.py b/src/conf_mode/interfaces-bridge.py
new file mode 100755
index 000000000..ee8e85e73
--- /dev/null
+++ b/src/conf_mode/interfaces-bridge.py
@@ -0,0 +1,139 @@
+#!/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 netifaces import interfaces
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_vrf
+from vyos.ifconfig import BridgeIf
+from vyos.validate import is_member, has_address_configured
+from vyos.xml import defaults
+
+from vyos.util import cmd
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'bridge']
+ bridge = get_interface_dict(conf, base)
+
+ # determine which members have been removed
+ tmp = node_changed(conf, ['member', 'interface'])
+ if tmp:
+ if 'member' in bridge:
+ bridge['member'].update({'interface_remove': tmp })
+ else:
+ bridge.update({'member': {'interface_remove': tmp }})
+
+ if 'member' in bridge and 'interface' in bridge['member']:
+ # XXX TT2665 we need a copy of the dict keys for iteration, else we will get:
+ # RuntimeError: dictionary changed size during iteration
+ for interface in list(bridge['member']['interface']):
+ for key in ['cost', 'priority']:
+ if interface == key:
+ del bridge['member']['interface'][key]
+ continue
+
+ # the default dictionary is not properly paged into the dict (see T2665)
+ # thus we will ammend it ourself
+ default_member_values = defaults(base + ['member', 'interface'])
+ for interface, interface_config in bridge['member']['interface'].items():
+ interface_config.update(default_member_values)
+
+ # Check if we are a member of another bridge device
+ tmp = is_member(conf, interface, 'bridge')
+ if tmp and tmp != bridge['ifname']:
+ interface_config.update({'is_bridge_member' : tmp})
+
+ # Check if we are a member of a bond device
+ tmp = is_member(conf, interface, 'bonding')
+ if tmp:
+ interface_config.update({'is_bond_member' : tmp})
+
+ # Bridge members must not have an assigned address
+ tmp = has_address_configured(conf, interface)
+ if tmp:
+ interface_config.update({'has_address' : ''})
+
+ return bridge
+
+def verify(bridge):
+ if 'deleted' in bridge:
+ return None
+
+ verify_dhcpv6(bridge)
+ verify_vrf(bridge)
+
+ if 'member' in bridge:
+ member = bridge.get('member')
+ bridge_name = bridge['ifname']
+ for interface, interface_config in member.get('interface', {}).items():
+ error_msg = f'Can not add interface "{interface}" to bridge "{bridge_name}", '
+
+ if interface == 'lo':
+ raise ConfigError('Loopback interface "lo" can not be added to a bridge')
+
+ if interface not in interfaces():
+ raise ConfigError(error_msg + 'it does not exist!')
+
+ if 'is_bridge_member' in interface_config:
+ tmp = interface_config['is_bridge_member']
+ raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!')
+
+ if 'is_bond_member' in interface_config:
+ tmp = interface_config['is_bond_member']
+ raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!')
+
+ if 'has_address' in interface_config:
+ raise ConfigError(error_msg + 'it has an address assigned!')
+
+ return None
+
+def generate(bridge):
+ return None
+
+def apply(bridge):
+ br = BridgeIf(bridge['ifname'])
+ if 'deleted' in bridge:
+ # delete interface
+ br.remove()
+ else:
+ br.update(bridge)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-dummy.py b/src/conf_mode/interfaces-dummy.py
new file mode 100755
index 000000000..8df86c8ea
--- /dev/null
+++ b/src/conf_mode/interfaces-dummy.py
@@ -0,0 +1,73 @@
+#!/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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import DummyIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'dummy']
+ dummy = get_interface_dict(conf, base)
+ return dummy
+
+def verify(dummy):
+ if 'deleted' in dummy.keys():
+ verify_bridge_delete(dummy)
+ return None
+
+ verify_vrf(dummy)
+ verify_address(dummy)
+
+ return None
+
+def generate(dummy):
+ return None
+
+def apply(dummy):
+ d = DummyIf(dummy['ifname'])
+
+ # Remove dummy interface
+ if 'deleted' in dummy.keys():
+ d.remove()
+ else:
+ d.update(dummy)
+
+ 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-ethernet.py b/src/conf_mode/interfaces-ethernet.py
new file mode 100755
index 000000000..10758e35a
--- /dev/null
+++ b/src/conf_mode/interfaces-ethernet.py
@@ -0,0 +1,89 @@
+#!/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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configverify import verify_interface_exists
+from vyos.configverify import verify_dhcpv6
+from vyos.configverify import verify_address
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_vlan_config
+from vyos.ifconfig import EthernetIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'ethernet']
+ ethernet = get_interface_dict(conf, base)
+ return ethernet
+
+def verify(ethernet):
+ if 'deleted' in ethernet:
+ return None
+
+ verify_interface_exists(ethernet)
+
+ if ethernet.get('speed', None) == 'auto':
+ if ethernet.get('duplex', None) != 'auto':
+ raise ConfigError('If speed is hardcoded, duplex must be hardcoded, too')
+
+ if ethernet.get('duplex', None) == 'auto':
+ if ethernet.get('speed', None) != 'auto':
+ raise ConfigError('If duplex is hardcoded, speed must be hardcoded, too')
+
+ verify_dhcpv6(ethernet)
+ verify_address(ethernet)
+ verify_vrf(ethernet)
+
+ if {'is_bond_member', 'mac'} <= set(ethernet):
+ print(f'WARNING: changing mac address "{mac}" will be ignored as "{ifname}" '
+ f'is a member of bond "{is_bond_member}"'.format(**ethernet))
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(ethernet)
+ return None
+
+def generate(ethernet):
+ return None
+
+def apply(ethernet):
+ e = EthernetIf(ethernet['ifname'])
+ if 'deleted' in ethernet:
+ # delete interface
+ e.remove()
+ else:
+ e.update(ethernet)
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-geneve.py b/src/conf_mode/interfaces-geneve.py
new file mode 100755
index 000000000..1104bd3c0
--- /dev/null
+++ b/src/conf_mode/interfaces-geneve.py
@@ -0,0 +1,96 @@
+#!/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_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import GeneveIf
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'geneve']
+ geneve = get_interface_dict(conf, base)
+ return geneve
+
+def verify(geneve):
+ if 'deleted' in geneve:
+ verify_bridge_delete(geneve)
+ return None
+
+ verify_address(geneve)
+
+ if 'remote' not in geneve:
+ raise ConfigError('Remote side must be configured')
+
+ if 'vni' not in geneve:
+ raise ConfigError('VNI must be configured')
+
+ return None
+
+
+def generate(geneve):
+ return None
+
+
+def apply(geneve):
+ # Check if GENEVE interface already exists
+ if geneve['ifname'] in interfaces():
+ g = GeneveIf(geneve['ifname'])
+ # GENEVE is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ g.remove()
+
+ if 'deleted' not in geneve:
+ # GENEVE interface needs to be created on-block
+ # instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(GeneveIf.get_config())
+
+ # Assign GENEVE instance configuration parameters to config dict
+ conf['vni'] = geneve['vni']
+ conf['remote'] = geneve['remote']
+
+ # Finally create the new interface
+ g = GeneveIf(geneve['ifname'], **conf)
+ g.update(geneve)
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-l2tpv3.py b/src/conf_mode/interfaces-l2tpv3.py
new file mode 100755
index 000000000..0978df5b6
--- /dev/null
+++ b/src/conf_mode/interfaces-l2tpv3.py
@@ -0,0 +1,127 @@
+#!/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.configdict import leaf_node_changed
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import L2TPv3If
+from vyos.util import check_kmod
+from vyos.validate import is_addr_assigned
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+k_mod = ['l2tp_eth', 'l2tp_netlink', 'l2tp_ip', 'l2tp_ip6']
+
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'l2tpv3']
+ l2tpv3 = get_interface_dict(conf, base)
+
+ # L2TPv3 is "special" the default MTU is 1488 - 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:
+ l2tpv3['mtu'] = '1488'
+
+ # To delete an l2tpv3 interface we need the current tunnel and session-id
+ if 'deleted' in l2tpv3:
+ tmp = leaf_node_changed(conf, ['tunnel-id'])
+ l2tpv3.update({'tunnel_id': tmp})
+
+ tmp = leaf_node_changed(conf, ['session-id'])
+ l2tpv3.update({'session_id': tmp})
+
+ return l2tpv3
+
+def verify(l2tpv3):
+ if 'deleted' in l2tpv3:
+ verify_bridge_delete(l2tpv3)
+ return None
+
+ interface = l2tpv3['ifname']
+
+ for key in ['local_ip', 'remote_ip', 'tunnel_id', 'peer_tunnel_id',
+ 'session_id', 'peer_session_id']:
+ if key not in l2tpv3:
+ tmp = key.replace('_', '-')
+ raise ConfigError(f'L2TPv3 {tmp} must be configured!')
+
+ if not is_addr_assigned(l2tpv3['local_ip']):
+ raise ConfigError('L2TPv3 local-ip address '
+ '"{local_ip}" is not configured!'.format(**l2tpv3))
+
+ verify_address(l2tpv3)
+ return None
+
+def generate(l2tpv3):
+ return None
+
+def apply(l2tpv3):
+ # L2TPv3 interface needs to be created/deleted on-block, instead of
+ # passing a ton of arguments, I just use a dict that is managed by
+ # vyos.ifconfig
+ conf = deepcopy(L2TPv3If.get_config())
+
+ # Check if L2TPv3 interface already exists
+ if l2tpv3['ifname'] in interfaces():
+ # L2TPv3 is picky when changing tunnels/sessions, thus we can simply
+ # always delete it first.
+ conf['session_id'] = l2tpv3['session_id']
+ conf['tunnel_id'] = l2tpv3['tunnel_id']
+ l = L2TPv3If(l2tpv3['ifname'], **conf)
+ l.remove()
+
+ if 'deleted' not in l2tpv3:
+ conf['peer_tunnel_id'] = l2tpv3['peer_tunnel_id']
+ conf['local_port'] = l2tpv3['source_port']
+ conf['remote_port'] = l2tpv3['destination_port']
+ conf['encapsulation'] = l2tpv3['encapsulation']
+ conf['local_address'] = l2tpv3['local_ip']
+ conf['remote_address'] = l2tpv3['remote_ip']
+ conf['session_id'] = l2tpv3['session_id']
+ conf['tunnel_id'] = l2tpv3['tunnel_id']
+ conf['peer_session_id'] = l2tpv3['peer_session_id']
+
+ # Finally create the new interface
+ l = L2TPv3If(l2tpv3['ifname'], **conf)
+ l.update(l2tpv3)
+
+ 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/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py
new file mode 100755
index 000000000..0398cd591
--- /dev/null
+++ b/src/conf_mode/interfaces-loopback.py
@@ -0,0 +1,61 @@
+#!/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 vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.ifconfig import LoopbackIf
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'loopback']
+ loopback = get_interface_dict(conf, base)
+ return loopback
+
+def verify(loopback):
+ return None
+
+def generate(loopback):
+ return None
+
+def apply(loopback):
+ l = LoopbackIf(loopback['ifname'])
+ if 'deleted' in loopback.keys():
+ l.remove()
+ else:
+ l.update(loopback)
+
+ 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-macsec.py b/src/conf_mode/interfaces-macsec.py
new file mode 100755
index 000000000..ca15212d4
--- /dev/null
+++ b/src/conf_mode/interfaces-macsec.py
@@ -0,0 +1,130 @@
+#!/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 copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.ifconfig import MACsecIf
+from vyos.template import render
+from vyos.util import call
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+# XXX: wpa_supplicant works on the source interface
+wpa_suppl_conf = '/run/wpa_supplicant/{source_interface}.conf'
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'macsec']
+ macsec = get_interface_dict(conf, base)
+
+ # Check if interface has been removed
+ if 'deleted' in macsec:
+ source_interface = conf.return_effective_value(
+ base + ['source-interface'])
+ macsec.update({'source_interface': source_interface})
+
+ return macsec
+
+
+def verify(macsec):
+ if 'deleted' in macsec:
+ verify_bridge_delete(macsec)
+ return None
+
+ verify_source_interface(macsec)
+ verify_vrf(macsec)
+ verify_address(macsec)
+
+ if not (('security' in macsec) and
+ ('cipher' in macsec['security'])):
+ raise ConfigError(
+ 'Cipher suite must be set for MACsec "{ifname}"'.format(**macsec))
+
+ if (('security' in macsec) and
+ ('encrypt' in macsec['security'])):
+ tmp = macsec.get('security')
+
+ if not (('mka' in tmp) and
+ ('cak' in tmp['mka']) and
+ ('ckn' in tmp['mka'])):
+ raise ConfigError('Missing mandatory MACsec security '
+ 'keys as encryption is enabled!')
+
+ return None
+
+
+def generate(macsec):
+ render(wpa_suppl_conf.format(**macsec),
+ 'macsec/wpa_supplicant.conf.tmpl', macsec)
+ return None
+
+
+def apply(macsec):
+ # Remove macsec interface
+ if 'deleted' in macsec.keys():
+ call('systemctl stop wpa_supplicant-macsec@{source_interface}'
+ .format(**macsec))
+
+ MACsecIf(macsec['ifname']).remove()
+
+ # delete configuration on interface removal
+ if os.path.isfile(wpa_suppl_conf.format(**macsec)):
+ os.unlink(wpa_suppl_conf.format(**macsec))
+
+ else:
+ # MACsec interfaces require a configuration when they are added using
+ # iproute2. This static method will provide the configuration
+ # dictionary used by this class.
+
+ # XXX: subject of removal after completing T2653
+ conf = deepcopy(MACsecIf.get_config())
+ conf['source_interface'] = macsec['source_interface']
+ conf['security_cipher'] = macsec['security']['cipher']
+
+ # It is safe to "re-create" the interface always, there is a sanity
+ # check that the interface will only be create if its non existent
+ i = MACsecIf(macsec['ifname'], **conf)
+ i.update(macsec)
+
+ call('systemctl restart wpa_supplicant-macsec@{source_interface}'
+ .format(**macsec))
+
+ 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
new file mode 100755
index 000000000..1420b4116
--- /dev/null
+++ b/src/conf_mode/interfaces-openvpn.py
@@ -0,0 +1,1116 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+
+from copy import deepcopy
+from sys import exit,stderr
+from ipaddress import ip_address,ip_network,IPv4Address,IPv4Network,IPv6Address,IPv6Network,summarize_address_range
+from netifaces import interfaces
+from time import sleep
+from shutil import rmtree
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos.ifconfig import VTunIf
+from vyos.template import render
+from vyos.util import call, chown, chmod_600, chmod_755
+from vyos.validate import is_addr_assigned, is_member, is_ipv4
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+user = 'openvpn'
+group = 'openvpn'
+
+default_config_data = {
+ 'address': [],
+ 'auth_user': '',
+ 'auth_pass': '',
+ 'auth_user_pass_file': '',
+ 'auth': False,
+ 'compress_lzo': False,
+ 'deleted': False,
+ 'description': '',
+ 'disable': False,
+ 'disable_ncp': False,
+ 'encryption': '',
+ 'hash': '',
+ 'intf': '',
+ 'ipv6_accept_ra': 1,
+ 'ipv6_autoconf': 0,
+ 'ipv6_eui64_prefix': [],
+ 'ipv6_eui64_prefix_remove': [],
+ 'ipv6_forwarding': 1,
+ 'ipv6_dup_addr_detect': 1,
+ 'ipv6_local_address': [],
+ 'ipv6_remote_address': [],
+ 'is_bridge_member': False,
+ 'ping_restart': '60',
+ 'ping_interval': '10',
+ 'local_address': [],
+ 'local_address_subnet': '',
+ 'local_host': '',
+ 'local_port': '',
+ 'mode': '',
+ 'ncp_ciphers': '',
+ 'options': [],
+ 'persistent_tunnel': False,
+ 'protocol': 'udp',
+ 'protocol_real': '',
+ 'redirect_gateway': '',
+ 'remote_address': [],
+ 'remote_host': [],
+ 'remote_port': '',
+ 'client': [],
+ 'server_domain': '',
+ 'server_max_conn': '',
+ 'server_dns_nameserver': [],
+ 'server_pool': True,
+ 'server_pool_start': '',
+ 'server_pool_stop': '',
+ 'server_pool_netmask': '',
+ 'server_push_route': [],
+ 'server_reject_unconfigured': False,
+ 'server_subnet': [],
+ 'server_topology': '',
+ 'server_ipv6_dns_nameserver': [],
+ 'server_ipv6_local': '',
+ 'server_ipv6_prefixlen': '',
+ 'server_ipv6_remote': '',
+ 'server_ipv6_pool': True,
+ 'server_ipv6_pool_base': '',
+ 'server_ipv6_pool_prefixlen': '',
+ 'server_ipv6_push_route': [],
+ 'server_ipv6_subnet': [],
+ 'shared_secret_file': '',
+ 'tls': False,
+ 'tls_auth': '',
+ 'tls_ca_cert': '',
+ 'tls_cert': '',
+ 'tls_crl': '',
+ 'tls_dh': '',
+ 'tls_key': '',
+ 'tls_crypt': '',
+ 'tls_role': '',
+ 'tls_version_min': '',
+ 'type': 'tun',
+ 'uid': user,
+ 'gid': group,
+ 'vrf': ''
+}
+
+
+def get_config_name(intf):
+ cfg_file = f'/run/openvpn/{intf}.conf'
+ return cfg_file
+
+
+def checkCertHeader(header, filename):
+ """
+ Verify if filename contains specified header.
+ Returns True if match is found, False if no match or file is not found
+ """
+ if not os.path.isfile(filename):
+ return False
+
+ with open(filename, 'r') as f:
+ for line in f:
+ if re.match(header, line):
+ return True
+
+ return False
+
+def getDefaultServer(network, topology, devtype):
+ """
+ Gets the default server parameters for a IPv4 "server" directive.
+ Logic from openvpn's src/openvpn/helper.c.
+ Returns a dict with addresses or False if the input parameters were incorrect.
+ """
+ if not (devtype == 'tun' or devtype == 'tap'):
+ return False
+
+ if not network.version == 4:
+ return False
+ elif (devtype == 'tun' and network.prefixlen > 29) or (devtype == 'tap' and network.prefixlen > 30):
+ return False
+
+ server = {
+ 'local': '',
+ 'remote_netmask': '',
+ 'client_remote_netmask': '',
+ 'pool_start': '',
+ 'pool_stop': '',
+ 'pool_netmask': ''
+ }
+
+ if devtype == 'tun':
+ if topology == 'net30' or topology == 'point-to-point':
+ server['local'] = network[1]
+ server['remote_netmask'] = network[2]
+ server['client_remote_netmask'] = server['local']
+
+ # pool start is 4th host IP in subnet (.4 in a /24)
+ server['pool_start'] = network[4]
+
+ if network.prefixlen == 29:
+ server['pool_stop'] = network.broadcast_address
+ else:
+ # pool end is -4 from the broadcast address (.251 in a /24)
+ server['pool_stop'] = network[-5]
+
+ elif topology == 'subnet':
+ server['local'] = network[1]
+ server['remote_netmask'] = str(network.netmask)
+ server['client_remote_netmask'] = server['remote_netmask']
+ server['pool_start'] = network[2]
+ server['pool_stop'] = network[-3]
+ server['pool_netmask'] = server['remote_netmask']
+
+ elif devtype == 'tap':
+ server['local'] = network[1]
+ server['remote_netmask'] = str(network.netmask)
+ server['client_remote_netmask'] = server['remote_netmask']
+ server['pool_start'] = network[2]
+ server['pool_stop'] = network[-2]
+ server['pool_netmask'] = server['remote_netmask']
+
+ return server
+
+def get_config():
+ openvpn = deepcopy(default_config_data)
+ conf = Config()
+
+ # determine tagNode instance
+ if 'VYOS_TAGNODE_VALUE' not in os.environ:
+ raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
+
+ openvpn['intf'] = os.environ['VYOS_TAGNODE_VALUE']
+ openvpn['auth_user_pass_file'] = f"/run/openvpn/{openvpn['intf']}.pw"
+
+ # check if interface is member of a bridge
+ openvpn['is_bridge_member'] = is_member(conf, openvpn['intf'], 'bridge')
+
+ # Check if interface instance has been removed
+ if not conf.exists('interfaces openvpn ' + openvpn['intf']):
+ openvpn['deleted'] = True
+ return openvpn
+
+ # bridged server should not have a pool by default (but can be specified manually)
+ if openvpn['is_bridge_member']:
+ openvpn['server_pool'] = False
+ openvpn['server_ipv6_pool'] = False
+
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # retrieve authentication options - username
+ if conf.exists('authentication username'):
+ openvpn['auth_user'] = conf.return_value('authentication username')
+ openvpn['auth'] = True
+
+ # retrieve authentication options - username
+ if conf.exists('authentication password'):
+ openvpn['auth_pass'] = conf.return_value('authentication password')
+ openvpn['auth'] = True
+
+ # retrieve interface description
+ if conf.exists('description'):
+ openvpn['description'] = conf.return_value('description')
+
+ # interface device-type
+ if conf.exists('device-type'):
+ openvpn['type'] = conf.return_value('device-type')
+
+ # disable interface
+ if conf.exists('disable'):
+ openvpn['disable'] = True
+
+ # data encryption algorithm cipher
+ if conf.exists('encryption cipher'):
+ openvpn['encryption'] = conf.return_value('encryption cipher')
+
+ # disable ncp-ciphers support
+ if conf.exists('encryption disable-ncp'):
+ openvpn['disable_ncp'] = True
+
+ # data encryption algorithm ncp-list
+ if conf.exists('encryption ncp-ciphers'):
+ _ncp_ciphers = []
+ for enc in conf.return_values('encryption ncp-ciphers'):
+ if enc == 'des':
+ _ncp_ciphers.append('des-cbc')
+ _ncp_ciphers.append('DES-CBC')
+ elif enc == '3des':
+ _ncp_ciphers.append('des-ede3-cbc')
+ _ncp_ciphers.append('DES-EDE3-CBC')
+ elif enc == 'aes128':
+ _ncp_ciphers.append('aes-128-cbc')
+ _ncp_ciphers.append('AES-128-CBC')
+ elif enc == 'aes128gcm':
+ _ncp_ciphers.append('aes-128-gcm')
+ _ncp_ciphers.append('AES-128-GCM')
+ elif enc == 'aes192':
+ _ncp_ciphers.append('aes-192-cbc')
+ _ncp_ciphers.append('AES-192-CBC')
+ elif enc == 'aes192gcm':
+ _ncp_ciphers.append('aes-192-gcm')
+ _ncp_ciphers.append('AES-192-GCM')
+ elif enc == 'aes256':
+ _ncp_ciphers.append('aes-256-cbc')
+ _ncp_ciphers.append('AES-256-CBC')
+ elif enc == 'aes256gcm':
+ _ncp_ciphers.append('aes-256-gcm')
+ _ncp_ciphers.append('AES-256-GCM')
+ openvpn['ncp_ciphers'] = ':'.join(_ncp_ciphers)
+
+ # hash algorithm
+ if conf.exists('hash'):
+ openvpn['hash'] = conf.return_value('hash')
+
+ # Maximum number of keepalive packet failures
+ if conf.exists('keep-alive failure-count') and conf.exists('keep-alive interval'):
+ fail_count = conf.return_value('keep-alive failure-count')
+ interval = conf.return_value('keep-alive interval')
+ openvpn['ping_interval' ] = interval
+ openvpn['ping_restart' ] = int(interval) * int(fail_count)
+
+ # Local IP address of tunnel - even as it is a tag node - we can only work
+ # on the first address
+ if conf.exists('local-address'):
+ for tmp in conf.list_nodes('local-address'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ openvpn['local_address'].append(tmp)
+ if conf.exists('local-address {} subnet-mask'.format(tmp)):
+ openvpn['local_address_subnet'] = conf.return_value('local-address {} subnet-mask'.format(tmp))
+ elif tmp_ip.version == 6:
+ # input IPv6 address could be expanded so get the compressed version
+ openvpn['ipv6_local_address'].append(str(tmp_ip))
+
+ # Local IP address to accept connections
+ if conf.exists('local-host'):
+ openvpn['local_host'] = conf.return_value('local-host')
+
+ # Local port number to accept connections
+ if conf.exists('local-port'):
+ openvpn['local_port'] = conf.return_value('local-port')
+
+ # Enable acquisition of IPv6 address using stateless autoconfig (SLAAC)
+ if conf.exists('ipv6 address autoconf'):
+ openvpn['ipv6_autoconf'] = 1
+
+ # Get prefixes for IPv6 addressing based on MAC address (EUI-64)
+ if conf.exists('ipv6 address eui64'):
+ openvpn['ipv6_eui64_prefix'] = conf.return_values('ipv6 address eui64')
+
+ # Determine currently effective EUI64 addresses - to determine which
+ # address is no longer valid and needs to be removed
+ eff_addr = conf.return_effective_values('ipv6 address eui64')
+ openvpn['ipv6_eui64_prefix_remove'] = list_diff(eff_addr, openvpn['ipv6_eui64_prefix'])
+
+ # Remove the default link-local address if set.
+ if conf.exists('ipv6 address no-default-link-local'):
+ openvpn['ipv6_eui64_prefix_remove'].append('fe80::/64')
+ else:
+ # add the link-local by default to make IPv6 work
+ openvpn['ipv6_eui64_prefix'].append('fe80::/64')
+
+ # Disable IPv6 forwarding on this interface
+ if conf.exists('ipv6 disable-forwarding'):
+ openvpn['ipv6_forwarding'] = 0
+
+ # IPv6 Duplicate Address Detection (DAD) tries
+ if conf.exists('ipv6 dup-addr-detect-transmits'):
+ openvpn['ipv6_dup_addr_detect'] = int(conf.return_value('ipv6 dup-addr-detect-transmits'))
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if openvpn['ipv6_autoconf'] or 'dhcpv6' in openvpn['address']:
+ openvpn['ipv6_accept_ra'] = 2
+
+ # OpenVPN operation mode
+ if conf.exists('mode'):
+ openvpn['mode'] = conf.return_value('mode')
+
+ # Additional OpenVPN options
+ if conf.exists('openvpn-option'):
+ openvpn['options'] = conf.return_values('openvpn-option')
+
+ # Do not close and reopen interface
+ if conf.exists('persistent-tunnel'):
+ openvpn['persistent_tunnel'] = True
+
+ # Communication protocol
+ if conf.exists('protocol'):
+ openvpn['protocol'] = conf.return_value('protocol')
+
+ # IP address of remote end of tunnel
+ if conf.exists('remote-address'):
+ for tmp in conf.return_values('remote-address'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ openvpn['remote_address'].append(tmp)
+ elif tmp_ip.version == 6:
+ openvpn['ipv6_remote_address'].append(str(tmp_ip))
+
+ # Remote host to connect to (dynamic if not set)
+ if conf.exists('remote-host'):
+ openvpn['remote_host'] = conf.return_values('remote-host')
+
+ # Remote port number to connect to
+ if conf.exists('remote-port'):
+ openvpn['remote_port'] = conf.return_value('remote-port')
+
+ # OpenVPN tunnel to be used as the default route
+ # see https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/
+ # redirect-gateway flags
+ if conf.exists('replace-default-route'):
+ openvpn['redirect_gateway'] = 'def1'
+
+ if conf.exists('replace-default-route local'):
+ openvpn['redirect_gateway'] = 'local def1'
+
+ # Topology for clients
+ if conf.exists('server topology'):
+ openvpn['server_topology'] = conf.return_value('server topology')
+
+ # Server-mode subnet (from which client IPs are allocated)
+ server_network_v4 = None
+ server_network_v6 = None
+ if conf.exists('server subnet'):
+ for tmp in conf.return_values('server subnet'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ server_network_v4 = tmp_ip
+ # convert the network to format: "192.0.2.0 255.255.255.0" for later use in template
+ openvpn['server_subnet'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ server_network_v6 = tmp_ip
+ openvpn['server_ipv6_subnet'].append(str(tmp_ip))
+
+ # Client-specific settings
+ for client in conf.list_nodes('server client'):
+ # set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client ' + client)
+ data = {
+ 'name': client,
+ 'disable': False,
+ 'ip': [],
+ 'ipv6_ip': [],
+ 'ipv6_remote': '',
+ 'ipv6_push_route': [],
+ 'ipv6_subnet': [],
+ 'push_route': [],
+ 'subnet': [],
+ 'remote_netmask': ''
+ }
+
+ # Option to disable client connection
+ if conf.exists('disable'):
+ data['disable'] = True
+
+ # IP address of the client
+ for tmp in conf.return_values('ip'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ data['ip'].append(tmp)
+ elif tmp_ip.version == 6:
+ data['ipv6_ip'].append(str(tmp_ip))
+
+ # Route to be pushed to the client
+ for tmp in conf.return_values('push-route'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ data['push_route'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ data['ipv6_push_route'].append(str(tmp_ip))
+
+ # Subnet belonging to the client
+ for tmp in conf.return_values('subnet'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ data['subnet'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ data['ipv6_subnet'].append(str(tmp_ip))
+
+ # Append to global client list
+ openvpn['client'].append(data)
+
+ # re-set configuration level
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # Server client IP pool
+ if conf.exists('server client-ip-pool'):
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ip-pool')
+
+ # enable or disable server_pool where necessary
+ # default is enabled, or disabled in bridge mode
+ openvpn['server_pool'] = not conf.exists('disable')
+
+ if conf.exists('start'):
+ openvpn['server_pool_start'] = conf.return_value('start')
+
+ if conf.exists('stop'):
+ openvpn['server_pool_stop'] = conf.return_value('stop')
+
+ if conf.exists('netmask'):
+ openvpn['server_pool_netmask'] = conf.return_value('netmask')
+
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # Server client IPv6 pool
+ if conf.exists('server client-ipv6-pool'):
+ conf.set_level('interfaces openvpn ' + openvpn['intf'] + ' server client-ipv6-pool')
+ openvpn['server_ipv6_pool'] = not conf.exists('disable')
+ if conf.exists('base'):
+ tmp = conf.return_value('base').split('/')
+ openvpn['server_ipv6_pool_base'] = str(IPv6Address(tmp[0]))
+ if 1 < len(tmp):
+ openvpn['server_ipv6_pool_prefixlen'] = tmp[1]
+
+ conf.set_level('interfaces openvpn ' + openvpn['intf'])
+
+ # DNS suffix to be pushed to all clients
+ if conf.exists('server domain-name'):
+ openvpn['server_domain'] = conf.return_value('server domain-name')
+
+ # Number of maximum client connections
+ if conf.exists('server max-connections'):
+ openvpn['server_max_conn'] = conf.return_value('server max-connections')
+
+ # Domain Name Server (DNS)
+ if conf.exists('server name-server'):
+ for tmp in conf.return_values('server name-server'):
+ tmp_ip = ip_address(tmp)
+ if tmp_ip.version == 4:
+ openvpn['server_dns_nameserver'].append(tmp)
+ elif tmp_ip.version == 6:
+ openvpn['server_ipv6_dns_nameserver'].append(str(tmp_ip))
+
+ # Route to be pushed to all clients
+ if conf.exists('server push-route'):
+ for tmp in conf.return_values('server push-route'):
+ tmp_ip = ip_network(tmp)
+ if tmp_ip.version == 4:
+ openvpn['server_push_route'].append(tmp_ip.with_netmask.replace(r'/', ' '))
+ elif tmp_ip.version == 6:
+ openvpn['server_ipv6_push_route'].append(str(tmp_ip))
+
+ # Reject connections from clients that are not explicitly configured
+ if conf.exists('server reject-unconfigured-clients'):
+ openvpn['server_reject_unconfigured'] = True
+
+ # File containing TLS auth static key
+ if conf.exists('tls auth-file'):
+ openvpn['tls_auth'] = conf.return_value('tls auth-file')
+ openvpn['tls'] = True
+
+ # File containing certificate for Certificate Authority (CA)
+ if conf.exists('tls ca-cert-file'):
+ openvpn['tls_ca_cert'] = conf.return_value('tls ca-cert-file')
+ openvpn['tls'] = True
+
+ # File containing certificate for this host
+ if conf.exists('tls cert-file'):
+ openvpn['tls_cert'] = conf.return_value('tls cert-file')
+ openvpn['tls'] = True
+
+ # File containing certificate revocation list (CRL) for this host
+ if conf.exists('tls crl-file'):
+ openvpn['tls_crl'] = conf.return_value('tls crl-file')
+ openvpn['tls'] = True
+
+ # File containing Diffie Hellman parameters (server only)
+ if conf.exists('tls dh-file'):
+ openvpn['tls_dh'] = conf.return_value('tls dh-file')
+ openvpn['tls'] = True
+
+ # File containing this host's private key
+ if conf.exists('tls key-file'):
+ openvpn['tls_key'] = conf.return_value('tls key-file')
+ openvpn['tls'] = True
+
+ # File containing key to encrypt control channel packets
+ if conf.exists('tls crypt-file'):
+ openvpn['tls_crypt'] = conf.return_value('tls crypt-file')
+ openvpn['tls'] = True
+
+ # Role in TLS negotiation
+ if conf.exists('tls role'):
+ openvpn['tls_role'] = conf.return_value('tls role')
+ openvpn['tls'] = True
+
+ # Minimum required TLS version
+ if conf.exists('tls tls-version-min'):
+ openvpn['tls_version_min'] = conf.return_value('tls tls-version-min')
+ openvpn['tls'] = True
+
+ if conf.exists('shared-secret-key-file'):
+ openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file')
+
+ if conf.exists('use-lzo-compression'):
+ openvpn['compress_lzo'] = True
+
+ # Special case when using EC certificates:
+ # if key-file is EC and dh-file is unset, set tls_dh to 'none'
+ if not openvpn['tls_dh'] and openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ openvpn['tls_dh'] = 'none'
+
+ # set default server topology to net30
+ if openvpn['mode'] == 'server' and not openvpn['server_topology']:
+ openvpn['server_topology'] = 'net30'
+
+ # Convert protocol to real protocol used by openvpn.
+ # To make openvpn listen on both IPv4 and IPv6 we must use *6 protocols
+ # (https://community.openvpn.net/openvpn/ticket/360), unless the local-host
+ # or each of the remote-host in client mode is IPv4
+ # in which case it must use the standard protocols.
+ if openvpn['protocol'] == 'tcp-active':
+ openvpn['protocol_real'] = 'tcp6-client'
+ elif openvpn['protocol'] == 'tcp-passive':
+ openvpn['protocol_real'] = 'tcp6-server'
+ else:
+ openvpn['protocol_real'] = 'udp6'
+
+ if ( is_ipv4(openvpn['local_host']) or
+ # in client mode test all the remotes instead
+ (openvpn['mode'] == 'client' and all([is_ipv4(h) for h in openvpn['remote_host']])) ):
+ # takes out the '6'
+ openvpn['protocol_real'] = openvpn['protocol_real'][:3] + openvpn['protocol_real'][4:]
+
+ # Set defaults where necessary.
+ # If any of the input parameters are wrong,
+ # this will return False and no defaults will be set.
+ if server_network_v4 and openvpn['server_topology'] and openvpn['type']:
+ default_server = None
+ default_server = getDefaultServer(server_network_v4, openvpn['server_topology'], openvpn['type'])
+ if default_server:
+ # server-bridge doesn't require a pool so don't set defaults for it
+ if openvpn['server_pool'] and not openvpn['is_bridge_member']:
+ if not openvpn['server_pool_start']:
+ openvpn['server_pool_start'] = default_server['pool_start']
+
+ if not openvpn['server_pool_stop']:
+ openvpn['server_pool_stop'] = default_server['pool_stop']
+
+ if not openvpn['server_pool_netmask']:
+ openvpn['server_pool_netmask'] = default_server['pool_netmask']
+
+ for client in openvpn['client']:
+ client['remote_netmask'] = default_server['client_remote_netmask']
+
+ if server_network_v6:
+ if not openvpn['server_ipv6_local']:
+ openvpn['server_ipv6_local'] = server_network_v6[1]
+ if not openvpn['server_ipv6_prefixlen']:
+ openvpn['server_ipv6_prefixlen'] = server_network_v6.prefixlen
+ if not openvpn['server_ipv6_remote']:
+ openvpn['server_ipv6_remote'] = server_network_v6[2]
+
+ if openvpn['server_ipv6_pool'] and server_network_v6.prefixlen < 112:
+ if not openvpn['server_ipv6_pool_base']:
+ openvpn['server_ipv6_pool_base'] = server_network_v6[0x1000]
+ if not openvpn['server_ipv6_pool_prefixlen']:
+ openvpn['server_ipv6_pool_prefixlen'] = openvpn['server_ipv6_prefixlen']
+
+ for client in openvpn['client']:
+ client['ipv6_remote'] = openvpn['server_ipv6_local']
+
+ if openvpn['redirect_gateway']:
+ openvpn['redirect_gateway'] += ' ipv6'
+
+ # retrieve VRF instance
+ if conf.exists('vrf'):
+ openvpn['vrf'] = conf.return_value('vrf')
+
+ return openvpn
+
+def verify(openvpn):
+ if openvpn['deleted']:
+ if openvpn['is_bridge_member']:
+ raise ConfigError((
+ f'Cannot delete interface "{openvpn["intf"]}" as it is a '
+ f'member of bridge "{openvpn["is_bridge_menber"]}"!'))
+ return None
+
+
+ if not openvpn['mode']:
+ raise ConfigError('Must specify OpenVPN operation mode')
+
+ # Check if we have disabled ncp and at the same time specified ncp-ciphers
+ if openvpn['disable_ncp'] and openvpn['ncp_ciphers']:
+ raise ConfigError('Cannot specify both "encryption disable-ncp" and "encryption ncp-ciphers"')
+ #
+ # OpenVPN client mode - VERIFY
+ #
+ if openvpn['mode'] == 'client':
+ if openvpn['local_port']:
+ raise ConfigError('Cannot specify "local-port" in client mode')
+
+ if openvpn['local_host']:
+ raise ConfigError('Cannot specify "local-host" in client mode')
+
+ if openvpn['protocol'] == 'tcp-passive':
+ raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
+
+ if not openvpn['remote_host']:
+ raise ConfigError('Must specify "remote-host" in client mode')
+
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ raise ConfigError('Cannot specify "tls dh-file" in client mode')
+
+ #
+ # OpenVPN site-to-site - VERIFY
+ #
+ if openvpn['mode'] == 'site-to-site':
+ if openvpn['ncp_ciphers']:
+ raise ConfigError('encryption ncp-ciphers cannot be specified in site-to-site mode, only server or client')
+
+ if openvpn['mode'] == 'site-to-site' and not openvpn['is_bridge_member']:
+ if not (openvpn['local_address'] or openvpn['ipv6_local_address']):
+ raise ConfigError('Must specify "local-address" or add interface to bridge')
+
+ if len(openvpn['local_address']) > 1 or len(openvpn['ipv6_local_address']) > 1:
+ raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "local-address"')
+
+ if len(openvpn['remote_address']) > 1 or len(openvpn['ipv6_remote_address']) > 1:
+ raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 "remote-address"')
+
+ for host in openvpn['remote_host']:
+ if host in openvpn['remote_address'] or host in openvpn['ipv6_remote_address']:
+ raise ConfigError('"remote-address" cannot be the same as "remote-host"')
+
+ if openvpn['local_address'] and not (openvpn['remote_address'] or openvpn['local_address_subnet']):
+ raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address" or IPv4 "local-address subnet"')
+
+ if openvpn['remote_address'] and not openvpn['local_address']:
+ raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
+
+ if openvpn['ipv6_local_address'] and not openvpn['ipv6_remote_address']:
+ raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
+
+ if openvpn['ipv6_remote_address'] and not openvpn['ipv6_local_address']:
+ raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
+
+ if openvpn['type'] == 'tun':
+ if not (openvpn['remote_address'] or openvpn['ipv6_remote_address']):
+ raise ConfigError('Must specify "remote-address"')
+
+ if ( (openvpn['local_address'] and openvpn['local_address'] == openvpn['remote_address']) or
+ (openvpn['ipv6_local_address'] and openvpn['ipv6_local_address'] == openvpn['ipv6_remote_address']) ):
+ raise ConfigError('"local-address" and "remote-address" cannot be the same')
+
+ if openvpn['local_host'] in openvpn['local_address'] or openvpn['local_host'] in openvpn['ipv6_local_address']:
+ raise ConfigError('"local-address" cannot be the same as "local-host"')
+
+ else:
+ # checks for client-server or site-to-site bridged
+ if openvpn['local_address'] or openvpn['ipv6_local_address'] or openvpn['remote_address'] or openvpn['ipv6_remote_address']:
+ raise ConfigError('Cannot specify "local-address" or "remote-address" in client-server or bridge mode')
+
+ #
+ # OpenVPN server mode - VERIFY
+ #
+ if openvpn['mode'] == 'server':
+ if openvpn['protocol'] == 'tcp-active':
+ raise ConfigError('Protocol "tcp-active" is not valid in server mode')
+
+ if openvpn['remote_port']:
+ raise ConfigError('Cannot specify "remote-port" in server mode')
+
+ if openvpn['remote_host']:
+ raise ConfigError('Cannot specify "remote-host" in server mode')
+
+ if openvpn['protocol'] == 'tcp-passive' and len(openvpn['remote_host']) > 1:
+ raise ConfigError('Cannot specify more than 1 "remote-host" with "tcp-passive"')
+
+ if not openvpn['tls_dh'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode')
+
+ if len(openvpn['server_subnet']) > 1 or len(openvpn['server_ipv6_subnet']) > 1:
+ raise ConfigError('Cannot specify more than 1 IPv4 and 1 IPv6 server subnet')
+
+ for client in openvpn['client']:
+ if len(client['ip']) > 1 or len(client['ipv6_ip']) > 1:
+ raise ConfigError(f'Server client "{client["name"]}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
+
+ if openvpn['server_subnet']:
+ subnet = IPv4Network(openvpn['server_subnet'][0].replace(' ', '/'))
+
+ if openvpn['type'] == 'tun' and subnet.prefixlen > 29:
+ raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
+ elif openvpn['type'] == 'tap' and subnet.prefixlen > 30:
+ raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
+
+ for client in openvpn['client']:
+ if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
+ raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
+
+ else:
+ if not openvpn['is_bridge_member']:
+ raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
+
+ if openvpn['server_pool']:
+ if not (openvpn['server_pool_start'] and openvpn['server_pool_stop']):
+ raise ConfigError('Server client-ip-pool requires both start and stop addresses in bridged mode')
+ else:
+ v4PoolStart = IPv4Address(openvpn['server_pool_start'])
+ v4PoolStop = IPv4Address(openvpn['server_pool_stop'])
+ if v4PoolStart > v4PoolStop:
+ raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
+
+ v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
+ if v4PoolSize >= 65536:
+ raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
+
+ v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
+ for client in openvpn['client']:
+ if client['ip']:
+ for v4PoolNet in v4PoolNets:
+ if IPv4Address(client['ip'][0]) in v4PoolNet:
+ print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.',
+ file=stderr)
+
+ if openvpn['server_ipv6_subnet']:
+ if not openvpn['server_subnet']:
+ raise ConfigError('IPv6 server requires an IPv4 server subnet')
+
+ if openvpn['server_ipv6_pool']:
+ if not openvpn['server_pool']:
+ raise ConfigError('IPv6 server pool requires an IPv4 server pool')
+
+ if int(openvpn['server_ipv6_pool_prefixlen']) >= 112:
+ raise ConfigError('IPv6 server pool must be larger than /112')
+
+ v6PoolStart = IPv6Address(openvpn['server_ipv6_pool_base'])
+ v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
+ v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
+ if v6PoolSize < v4PoolSize:
+ raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
+
+ v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
+ for client in openvpn['client']:
+ if client['ipv6_ip']:
+ for v6PoolNet in v6PoolNets:
+ if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
+ print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.',
+ file=stderr)
+
+ else:
+ if openvpn['server_ipv6_push_route']:
+ raise ConfigError('IPv6 push-route requires an IPv6 server subnet')
+
+ for client in openvpn ['client']:
+ if client['ipv6_ip']:
+ raise ConfigError(f'Server client "{client["name"]}" IPv6 IP requires an IPv6 server subnet')
+ if client['ipv6_push_route']:
+ raise ConfigError(f'Server client "{client["name"]} IPv6 push-route requires an IPv6 server subnet"')
+ if client['ipv6_subnet']:
+ raise ConfigError(f'Server client "{client["name"]} IPv6 subnet requires an IPv6 server subnet"')
+
+ else:
+ # checks for both client and site-to-site go here
+ if openvpn['server_reject_unconfigured']:
+ raise ConfigError('reject-unconfigured-clients is only supported in OpenVPN server mode')
+
+ if openvpn['server_topology']:
+ raise ConfigError('The "topology" option is only valid in server mode')
+
+ if (not openvpn['remote_host']) and openvpn['redirect_gateway']:
+ raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
+
+ #
+ # OpenVPN common verification section
+ # not depending on any operation mode
+ #
+
+ # verify specified IP address is present on any interface on this system
+ if openvpn['local_host']:
+ if not is_addr_assigned(openvpn['local_host']):
+ raise ConfigError('No interface on system with specified local-host IP address: {}'.format(openvpn['local_host']))
+
+ # TCP active
+ if openvpn['protocol'] == 'tcp-active':
+ if openvpn['local_port']:
+ raise ConfigError('Cannot specify "local-port" with "tcp-active"')
+
+ if not openvpn['remote_host']:
+ raise ConfigError('Must specify "remote-host" with "tcp-active"')
+
+ # shared secret and TLS
+ if not (openvpn['shared_secret_file'] or openvpn['tls']):
+ raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"')
+
+ if openvpn['shared_secret_file'] and openvpn['tls']:
+ raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"')
+
+ if openvpn['mode'] in ['client', 'server']:
+ if not openvpn['tls']:
+ raise ConfigError('Must specify "tls" in client-server mode')
+
+ #
+ # TLS/encryption
+ #
+ if openvpn['shared_secret_file']:
+ if openvpn['encryption'] in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
+ raise ConfigError('GCM encryption with shared-secret-key-file is not supported')
+
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['shared_secret_file']):
+ raise ConfigError('Specified shared-secret-key-file "{}" is not valid'.format(openvpn['shared_secret_file']))
+
+ if openvpn['tls']:
+ if not openvpn['tls_ca_cert']:
+ raise ConfigError('Must specify "tls ca-cert-file"')
+
+ if not (openvpn['mode'] == 'client' and openvpn['auth']):
+ if not openvpn['tls_cert']:
+ raise ConfigError('Must specify "tls cert-file"')
+
+ if not openvpn['tls_key']:
+ raise ConfigError('Must specify "tls key-file"')
+
+ if openvpn['tls_auth'] and openvpn['tls_crypt']:
+ raise ConfigError('TLS auth and crypt are mutually exclusive')
+
+ if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_ca_cert']):
+ raise ConfigError('Specified ca-cert-file "{}" is invalid'.format(openvpn['tls_ca_cert']))
+
+ if openvpn['tls_auth']:
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_auth']):
+ raise ConfigError('Specified auth-file "{}" is invalid'.format(openvpn['tls_auth']))
+
+ if openvpn['tls_cert']:
+ if not checkCertHeader('-----BEGIN CERTIFICATE-----', openvpn['tls_cert']):
+ raise ConfigError('Specified cert-file "{}" is invalid'.format(openvpn['tls_cert']))
+
+ if openvpn['tls_key']:
+ if not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', openvpn['tls_key']):
+ raise ConfigError('Specified key-file "{}" is not valid'.format(openvpn['tls_key']))
+
+ if openvpn['tls_crypt']:
+ if not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', openvpn['tls_crypt']):
+ raise ConfigError('Specified TLS crypt-file "{}" is invalid'.format(openvpn['tls_crypt']))
+
+ if openvpn['tls_crl']:
+ if not checkCertHeader('-----BEGIN X509 CRL-----', openvpn['tls_crl']):
+ raise ConfigError('Specified crl-file "{} not valid'.format(openvpn['tls_crl']))
+
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ if not checkCertHeader('-----BEGIN DH PARAMETERS-----', openvpn['tls_dh']):
+ raise ConfigError('Specified dh-file "{}" is not valid'.format(openvpn['tls_dh']))
+
+ if openvpn['tls_role']:
+ if openvpn['mode'] in ['client', 'server']:
+ if not openvpn['tls_auth']:
+ raise ConfigError('Cannot specify "tls role" in client-server mode')
+
+ if openvpn['tls_role'] == 'active':
+ if openvpn['protocol'] == 'tcp-passive':
+ raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
+
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"')
+
+ elif openvpn['tls_role'] == 'passive':
+ if openvpn['protocol'] == 'tcp-active':
+ raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
+
+ if not openvpn['tls_dh']:
+ raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"')
+
+ if openvpn['tls_key'] and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls_key']):
+ if openvpn['tls_dh'] and openvpn['tls_dh'] != 'none':
+ print('Warning: using dh-file and EC keys simultaneously will lead to DH ciphers being used instead of ECDH')
+ else:
+ print('Diffie-Hellman prime file is unspecified, assuming ECDH')
+
+ #
+ # Auth user/pass
+ #
+ if openvpn['auth']:
+ if not openvpn['auth_user']:
+ raise ConfigError('Username for authentication is missing')
+
+ if not openvpn['auth_pass']:
+ raise ConfigError('Password for authentication is missing')
+
+ if openvpn['vrf']:
+ if openvpn['vrf'] not in interfaces():
+ raise ConfigError(f'VRF "{openvpn["vrf"]}" does not exist')
+
+ if openvpn['is_bridge_member']:
+ raise ConfigError((
+ f'Interface "{openvpn["intf"]}" cannot be member of VRF '
+ f'"{openvpn["vrf"]}" and bridge "{openvpn["is_bridge_member"]}" '
+ f'at the same time!'))
+
+ return None
+
+def generate(openvpn):
+ interface = openvpn['intf']
+ directory = os.path.dirname(get_config_name(interface))
+
+ # 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 openvpn['deleted'] or openvpn['disable']:
+ return None
+
+ # create config directory on demand
+ directories = []
+ directories.append(f'{directory}/status')
+ directories.append(f'{directory}/ccd/{interface}')
+ for onedir in directories:
+ if not os.path.exists(onedir):
+ os.makedirs(onedir, 0o755)
+ chown(onedir, user, group)
+
+ # Fix file permissons for keys
+ fix_permissions = []
+ fix_permissions.append(openvpn['shared_secret_file'])
+ fix_permissions.append(openvpn['tls_key'])
+
+ # Generate User/Password authentication file
+ if openvpn['auth']:
+ with open(openvpn['auth_user_pass_file'], 'w') as f:
+ f.write('{}\n{}'.format(openvpn['auth_user'], openvpn['auth_pass']))
+ # also change permission on auth file
+ fix_permissions.append(openvpn['auth_user_pass_file'])
+
+ 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
+ for client in openvpn['client']:
+ client_file = os.path.join(ccd_dir, client['name'])
+ render(client_file, 'openvpn/client.conf.tmpl', client)
+ chown(client_file, user, group)
+
+ # we need to support quoting of raw parameters from OpenVPN CLI
+ # see https://phabricator.vyos.net/T1632
+ render(get_config_name(interface), 'openvpn/server.conf.tmpl', openvpn,
+ formater=lambda _: _.replace("&quot;", '"'))
+ chown(get_config_name(interface), user, group)
+
+ # Fixup file permissions
+ for file in fix_permissions:
+ chmod_600(file)
+
+ return None
+
+def apply(openvpn):
+ interface = openvpn['intf']
+ call(f'systemctl stop openvpn@{interface}.service')
+
+ # Do some cleanup when OpenVPN is disabled/deleted
+ if openvpn['deleted'] or openvpn['disable']:
+ # cleanup old configuration files
+ cleanup = []
+ cleanup.append(get_config_name(interface))
+ cleanup.append(openvpn['auth_user_pass_file'])
+
+ for file in cleanup:
+ if os.path.isfile(file):
+ os.unlink(file)
+
+ return None
+
+ # On configuration change we need to wait for the 'old' interface to
+ # vanish from the Kernel, if it is not gone, OpenVPN will report:
+ # ERROR: Cannot ioctl TUNSETIFF vtun10: Device or resource busy (errno=16)
+ while interface in interfaces():
+ sleep(0.250) # 250ms
+
+ # No matching OpenVPN process running - maybe it got killed or none
+ # existed - nevertheless, spawn new OpenVPN process
+ call(f'systemctl start openvpn@{interface}.service')
+
+ # better late then sorry ... but we can only set interface alias after
+ # OpenVPN has been launched and created the interface
+ cnt = 0
+ while interface not in interfaces():
+ # If VPN tunnel can't be established because the peer/server isn't
+ # (temporarily) available, the vtun interface never becomes registered
+ # with the kernel, and the commit would hang if there is no bail out
+ # condition
+ cnt += 1
+ if cnt == 50:
+ break
+
+ # sleep 250ms
+ sleep(0.250)
+
+ try:
+ # we need to catch the exception if the interface is not up due to
+ # reason stated above
+ o = VTunIf(interface)
+ # update interface description used e.g. within SNMP
+ o.set_alias(openvpn['description'])
+ # IPv6 accept RA
+ o.set_ipv6_accept_ra(openvpn['ipv6_accept_ra'])
+ # IPv6 address autoconfiguration
+ o.set_ipv6_autoconf(openvpn['ipv6_autoconf'])
+ # IPv6 forwarding
+ o.set_ipv6_forwarding(openvpn['ipv6_forwarding'])
+ # IPv6 Duplicate Address Detection (DAD) tries
+ o.set_ipv6_dad_messages(openvpn['ipv6_dup_addr_detect'])
+
+ # IPv6 EUI-based addresses - only in TAP mode (TUN's have no MAC)
+ # If MAC has changed, old EUI64 addresses won't get deleted,
+ # but this isn't easy to solve, so leave them.
+ # This is even more difficult as openvpn uses a random MAC for the
+ # initial interface creation, unless set by 'lladdr'.
+ # NOTE: right now the interface is always deleted. For future
+ # compatibility when tap's are not deleted, leave the del_ in
+ if openvpn['mode'] == 'tap':
+ for addr in openvpn['ipv6_eui64_prefix_remove']:
+ o.del_ipv6_eui64_address(addr)
+ for addr in openvpn['ipv6_eui64_prefix']:
+ o.add_ipv6_eui64_address(addr)
+
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not openvpn['is_bridge_member']:
+ o.set_vrf(openvpn['vrf'])
+
+ except:
+ pass
+
+ # TAP interface needs to be brought up explicitly
+ if openvpn['type'] == 'tap':
+ if not openvpn['disable']:
+ VTunIf(interface).set_admin_state('up')
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py
new file mode 100755
index 000000000..901ea769c
--- /dev/null
+++ b/src/conf_mode/interfaces-pppoe.py
@@ -0,0 +1,132 @@
+#!/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.template import render
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ 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)
+
+ 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)
+ # Create script for ip-pre-up.d
+ render(script_pppoe_pre_up, 'pppoe/ip-pre-up.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ip-up.d
+ render(script_pppoe_ip_up, 'pppoe/ip-up.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ip-down.d
+ render(script_pppoe_ip_down, 'pppoe/ip-down.script.tmpl',
+ pppoe, trim_blocks=True, permission=0o755)
+ # Create script for ipv6-up.d
+ render(script_pppoe_ipv6_up, 'pppoe/ipv6-up.script.tmpl',
+ pppoe, trim_blocks=True, 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)
+
+ 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-pseudo-ethernet.py b/src/conf_mode/interfaces-pseudo-ethernet.py
new file mode 100755
index 000000000..fe2d7b1be
--- /dev/null
+++ b/src/conf_mode/interfaces-pseudo-ethernet.py
@@ -0,0 +1,123 @@
+#!/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 copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import get_interface_dict
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos.configverify import verify_vlan_config
+from vyos.ifconfig import MACVLANIf
+from vyos.validate import is_member
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at
+ least the interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'pseudo-ethernet']
+ peth = get_interface_dict(conf, base)
+
+ mode = leaf_node_changed(conf, ['mode'])
+ if mode:
+ peth.update({'mode_old' : mode})
+
+ # Check if source-interface is member of a bridge device
+ if 'source_interface' in peth:
+ bridge = is_member(conf, peth['source_interface'], 'bridge')
+ if bridge:
+ peth.update({'source_interface_is_bridge_member' : bridge})
+
+ # Check if we are a member of a bond device
+ bond = is_member(conf, peth['source_interface'], 'bonding')
+ if bond:
+ peth.update({'source_interface_is_bond_member' : bond})
+
+ return peth
+
+def verify(peth):
+ if 'deleted' in peth:
+ verify_bridge_delete(peth)
+ return None
+
+ verify_source_interface(peth)
+ verify_vrf(peth)
+ verify_address(peth)
+
+ if 'source_interface_is_bridge_member' in peth:
+ raise ConfigError(
+ 'Source interface "{source_interface}" can not be used as it is already a '
+ 'member of bridge "{source_interface_is_bridge_member}"!'.format(**peth))
+
+ if 'source_interface_is_bond_member' in peth:
+ raise ConfigError(
+ 'Source interface "{source_interface}" can not be used as it is already a '
+ 'member of bond "{source_interface_is_bond_member}"!'.format(**peth))
+
+ # use common function to verify VLAN configuration
+ verify_vlan_config(peth)
+ return None
+
+def generate(peth):
+ return None
+
+def apply(peth):
+ if 'deleted' in peth:
+ # delete interface
+ MACVLANIf(peth['ifname']).remove()
+ return None
+
+ # Check if MACVLAN interface already exists. Parameters like the underlaying
+ # source-interface device or mode can not be changed on the fly and the
+ # interface needs to be recreated from the bottom.
+ if 'mode_old' in peth:
+ MACVLANIf(peth['ifname']).remove()
+
+ # MACVLAN interface needs to be created on-block instead of passing a ton
+ # of arguments, I just use a dict that is managed by vyos.ifconfig
+ conf = deepcopy(MACVLANIf.get_config())
+
+ # Assign MACVLAN instance configuration parameters to config dict
+ conf['source_interface'] = peth['source_interface']
+ conf['mode'] = peth['mode']
+
+ # It is safe to "re-create" the interface always, there is a sanity check
+ # that the interface will only be create if its non existent
+ p = MACVLANIf(peth['ifname'], **conf)
+ p.update(peth)
+ 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-tunnel.py b/src/conf_mode/interfaces-tunnel.py
new file mode 100755
index 000000000..ea15a7fb7
--- /dev/null
+++ b/src/conf_mode/interfaces-tunnel.py
@@ -0,0 +1,718 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import netifaces
+
+from sys import exit
+from copy import deepcopy
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.ifconfig import Interface, GREIf, GRETapIf, IPIPIf, IP6GREIf, IPIP6If, IP6IP6If, SitIf, Sit6RDIf
+from vyos.ifconfig.afi import IP4, IP6
+from vyos.configdict import list_diff
+from vyos.validate import is_ipv4, is_ipv6, is_member
+from vyos import ConfigError
+from vyos.dicts import FixedDict
+
+from vyos import airbag
+airbag.enable()
+
+
+class ConfigurationState(object):
+ """
+ The current API require a dict to be generated by get_config()
+ which is then consumed by verify(), generate() and apply()
+
+ ConfiguartionState is an helper class wrapping Config and providing
+ an common API to this dictionary structure
+
+ Its to_api() function return a dictionary containing three fields,
+ each a dict, called options, changes, actions.
+
+ options:
+
+ contains the configuration options for the dict and its value
+ {'options': {'commment': 'test'}} will be set if
+ 'set interface dummy dum1 description test' was used and
+ the key 'commment' is used to index the description info.
+
+ changes:
+
+ per key, let us know how the data was modified using one of the action
+ a special key called 'section' is used to indicate what happened to the
+ section. for example:
+
+ 'set interface dummy dum1 description test' when no interface was setup
+ will result in the following changes
+ {'changes': {'section': 'create', 'comment': 'create'}}
+
+ on an existing interface, depending if there was a description
+ 'set interface dummy dum1 description test' will result in one of
+ {'changes': {'comment': 'create'}} (not present before)
+ {'changes': {'comment': 'static'}} (unchanged)
+ {'changes': {'comment': 'modify'}} (changed from half)
+
+ and 'delete interface dummy dummy1 description' will result in:
+ {'changes': {'comment': 'delete'}}
+
+ actions:
+
+ for each action list the configuration key which were changes
+ in our example if we added the 'description' and added an IP we would have
+ {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}}
+
+ the actions are:
+ 'create': it did not exist previously and was created
+ 'modify': it did exist previously but its content changed
+ 'static': it did exist and did not change
+ 'delete': it was present but was removed from the configuration
+ 'absent': it was not and is not present
+ which for each field represent how it was modified since the last commit
+ """
+
+ def __init__(self, configuration, section, default):
+ """
+ initialise the class for a given configuration path:
+
+ >>> conf = ConfigurationState(conf, 'interfaces ethernet eth1')
+ all further references to get_value(s) and get_effective(s)
+ will be for this part of the configuration (eth1)
+ """
+ self._conf = configuration
+
+ self.default = deepcopy(default)
+ self.options = FixedDict(**default)
+ self.actions = {
+ 'create': [], # the key did not exist and was added
+ 'static': [], # the key exists and its value was not modfied
+ 'modify': [], # the key exists and its value was modified
+ 'absent': [], # the key is not present
+ 'delete': [], # the key was present and was deleted
+ }
+ self.changes = {}
+ if not self._conf.exists(section):
+ self.changes['section'] = 'delete'
+ elif self._conf.exists_effective(section):
+ self.changes['section'] = 'modify'
+ else:
+ self.changes['section'] = 'create'
+
+ self.set_level(section)
+
+ def set_level(self, lpath):
+ self.section = lpath
+ self._conf.set_level(lpath)
+
+ def _act(self, section):
+ """
+ Returns for a given configuration field determine what happened to it
+
+ 'create': it did not exist previously and was created
+ 'modify': it did exist previously but its content changed
+ 'static': it did exist and did not change
+ 'delete': it was present but was removed from the configuration
+ 'absent': it was not and is not present
+ """
+ if self._conf.exists(section):
+ if self._conf.exists_effective(section):
+ if self._conf.return_value(section) != self._conf.return_effective_value(section):
+ return 'modify'
+ return 'static'
+ return 'create'
+ else:
+ if self._conf.exists_effective(section):
+ return 'delete'
+ return 'absent'
+
+ def _action(self, name, key):
+ action = self._act(key)
+ self.changes[name] = action
+ self.actions[action].append(name)
+ return action
+
+ def _get(self, name, key, default, getter):
+ value = getter(key)
+ if not value:
+ if default:
+ self.options[name] = default
+ return
+ self.options[name] = self.default[name]
+ return
+ self.options[name] = value
+
+ def get_value(self, name, key, default=None):
+ """
+ >>> conf.get_value('comment', 'description')
+ will place the string of 'interface dummy description test'
+ into the dictionnary entry 'comment' using Config.return_value
+ (the data in the configuration to apply)
+ """
+ if self._action(name, key) in ('delete', 'absent'):
+ return
+ return self._get(name, key, default, self._conf.return_value)
+
+ def get_values(self, name, key, default=None):
+ """
+ >>> conf.get_values('addresses', 'address')
+ will place a list of the new IP present in 'interface dummy dum1 address'
+ into the dictionnary entry "-add" (here 'addresses-add') using
+ Config.return_values and will add the the one which were removed in into
+ the entry "-del" (here addresses-del')
+ """
+ add_name = f'{name}-add'
+
+ if self._action(add_name, key) in ('delete', 'absent'):
+ return
+
+ self._get(add_name, key, default, self._conf.return_values)
+
+ # get the effective values to determine which data is no longer valid
+ self.options['addresses-del'] = list_diff(
+ self._conf.return_effective_values('address'),
+ self.options['addresses-add']
+ )
+
+ def get_effective(self, name, key, default=None):
+ """
+ >>> conf.get_value('comment', 'description')
+ will place the string of 'interface dummy description test'
+ into the dictionnary entry 'comment' using Config.return_effective_value
+ (the data in the configuration to apply)
+ """
+ self._action(name, key)
+ return self._get(name, key, default, self._conf.return_effective_value)
+
+ def get_effectives(self, name, key, default=None):
+ """
+ >>> conf.get_effectives('addresses-add', 'address')
+ will place a list made of the IP present in 'interface ethernet eth1 address'
+ into the dictionnary entry 'addresses-add' using Config.return_effectives_value
+ (the data in the un-modified configuration)
+ """
+ self._action(name, key)
+ return self._get(name, key, default, self._conf.return_effectives_value)
+
+ def load(self, mapping):
+ """
+ load will take a dictionary defining how we wish the configuration
+ to be parsed and apply this definition to set the data.
+
+ >>> mapping = {
+ 'addresses-add' : ('address', True, None),
+ 'comment' : ('description', False, 'auto'),
+ }
+ >>> conf.load(mapping)
+
+ mapping is a dictionary where each key represents the name we wish
+ to have (such as 'addresses-add'), with a list a content representing
+ how the data should be parsed:
+ - the configuration section name
+ such as 'address' under 'interface ethernet eth1'
+ - boolean indicating if this data can have multiple values
+ for 'address', True, as multiple IPs can be set
+ for 'description', False, as it is a single string
+ - default represent the default value if absent from the configuration
+ 'None' indicate that no default should be set if the configuration
+ does not have the configuration section
+
+ """
+ for local_name, (config_name, multiple, default) in mapping.items():
+ if multiple:
+ self.get_values(local_name, config_name, default)
+ else:
+ self.get_value(local_name, config_name, default)
+
+ def remove_default(self,*options):
+ """
+ remove all the values which were not changed from the default
+ """
+ for option in options:
+ if not self._conf.exists(option):
+ del self.options[option]
+ continue
+
+ if self._conf.return_value(option) == self.default[option]:
+ del self.options[option]
+ continue
+
+ if self._conf.return_values(option) == self.default[option]:
+ del self.options[option]
+ continue
+
+ def as_dict(self, lpath):
+ l = self._conf.get_level()
+ self._conf.set_level([])
+ d = self._conf.get_config_dict(lpath)
+ # XXX: that not what I would have expected from get_config_dict
+ if lpath:
+ d = d[lpath[-1]]
+ # XXX: it should have provided me the content and not the key
+ self._conf.set_level(l)
+ return d
+
+ def to_api(self):
+ """
+ provide a dictionary with the generated data for the configuration
+ options: the configuration value for the key
+ changes: per key how they changed from the previous configuration
+ actions: per changes all the options which were changed
+ """
+ # as we have to use a dict() for the API for verify and apply the options
+ return {
+ 'options': self.options,
+ 'changes': self.changes,
+ 'actions': self.actions,
+ }
+
+
+default_config_data = {
+ # interface definition
+ 'vrf': '',
+ 'addresses-add': [],
+ 'addresses-del': [],
+ 'state': 'up',
+ 'dhcp-interface': '',
+ 'link_detect': 1,
+ 'ip': False,
+ 'ipv6': False,
+ 'nhrp': [],
+ 'arp_filter': 1,
+ 'arp_accept': 0,
+ 'arp_announce': 0,
+ 'arp_ignore': 0,
+ 'ipv6_accept_ra': 1,
+ 'ipv6_autoconf': 0,
+ 'ipv6_forwarding': 1,
+ 'ipv6_dad_transmits': 1,
+ # internal
+ 'interfaces': [],
+ 'tunnel': {},
+ 'bridge': '',
+ # the following names are exactly matching the name
+ # for the ip command and must not be changed
+ 'ifname': '',
+ 'type': '',
+ 'alias': '',
+ 'mtu': '1476',
+ 'local': '',
+ 'remote': '',
+ 'dev': '',
+ 'multicast': 'disable',
+ 'allmulticast': 'disable',
+ 'ttl': '255',
+ 'tos': 'inherit',
+ 'key': '',
+ 'encaplimit': '4',
+ 'flowlabel': 'inherit',
+ 'hoplimit': '64',
+ 'tclass': 'inherit',
+ '6rd-prefix': '',
+ '6rd-relay-prefix': '',
+}
+
+
+# dict name -> config name, multiple values, default
+mapping = {
+ 'type': ('encapsulation', False, None),
+ 'alias': ('description', False, None),
+ 'mtu': ('mtu', False, None),
+ 'local': ('local-ip', False, None),
+ 'remote': ('remote-ip', False, None),
+ 'multicast': ('multicast', False, None),
+ 'dev': ('source-interface', False, None),
+ 'ttl': ('parameters ip ttl', False, None),
+ 'tos': ('parameters ip tos', False, None),
+ 'key': ('parameters ip key', False, None),
+ 'encaplimit': ('parameters ipv6 encaplimit', False, None),
+ 'flowlabel': ('parameters ipv6 flowlabel', False, None),
+ 'hoplimit': ('parameters ipv6 hoplimit', False, None),
+ 'tclass': ('parameters ipv6 tclass', False, None),
+ '6rd-prefix': ('6rd-prefix', False, None),
+ '6rd-relay-prefix': ('6rd-relay-prefix', False, None),
+ 'dhcp-interface': ('dhcp-interface', False, None),
+ 'state': ('disable', False, 'down'),
+ 'link_detect': ('disable-link-detect', False, 2),
+ 'vrf': ('vrf', False, None),
+ 'addresses': ('address', True, None),
+ 'arp_filter': ('ip disable-arp-filter', False, 0),
+ 'arp_accept': ('ip enable-arp-accept', False, 1),
+ 'arp_announce': ('ip enable-arp-announce', False, 1),
+ 'arp_ignore': ('ip enable-arp-ignore', False, 1),
+ 'ipv6_autoconf': ('ipv6 address autoconf', False, 1),
+ 'ipv6_forwarding': ('ipv6 disable-forwarding', False, 0),
+ 'ipv6_dad_transmits:': ('ipv6 dup-addr-detect-transmits', False, None)
+}
+
+
+def get_class (options):
+ dispatch = {
+ 'gre': GREIf,
+ 'gre-bridge': GRETapIf,
+ 'ipip': IPIPIf,
+ 'ipip6': IPIP6If,
+ 'ip6ip6': IP6IP6If,
+ 'ip6gre': IP6GREIf,
+ 'sit': SitIf,
+ }
+
+ kls = dispatch[options['type']]
+ if options['type'] == 'gre' and not options['remote'] \
+ and not options['key'] and not options['multicast']:
+ # will use GreTapIf on GreIf deletion but it does not matter
+ return GRETapIf
+ elif options['type'] == 'sit' and options['6rd-prefix']:
+ # will use SitIf on Sit6RDIf deletion but it does not matter
+ return Sit6RDIf
+ return kls
+
+def get_interface_ip (ifname):
+ if not ifname:
+ return ''
+ try:
+ addrs = Interface(ifname).get_addr()
+ if addrs:
+ return addrs[0].split('/')[0]
+ except Exception:
+ return ''
+
+def get_afi (ip):
+ return IP6 if is_ipv6(ip) else IP4
+
+def ip_proto (afi):
+ return 6 if afi == IP6 else 4
+
+
+def get_config():
+ ifname = os.environ.get('VYOS_TAGNODE_VALUE','')
+ if not ifname:
+ raise ConfigError('Interface not specified')
+
+ config = Config()
+ conf = ConfigurationState(config, ['interfaces', 'tunnel ', ifname], default_config_data)
+ options = conf.options
+ changes = conf.changes
+ options['ifname'] = ifname
+
+ if changes['section'] == 'delete':
+ conf.get_effective('type', mapping['type'][0])
+ config.set_level(['protocols', 'nhrp', 'tunnel'])
+ options['nhrp'] = config.list_nodes('')
+ return conf.to_api()
+
+ # load all the configuration option according to the mapping
+ conf.load(mapping)
+
+ # remove default value if not set and not required
+ afi_local = get_afi(options['local'])
+ if afi_local == IP6:
+ conf.remove_default('ttl', 'tos', 'key')
+ if afi_local == IP4:
+ conf.remove_default('encaplimit', 'flowlabel', 'hoplimit', 'tclass')
+
+ # if the local-ip is not set, pick one from the interface !
+ # hopefully there is only one, otherwise it will not be very deterministic
+ # at time of writing the code currently returns ipv4 before ipv6 in the list
+
+ # XXX: There is no way to trigger an update of the interface source IP if
+ # XXX: the underlying interface IP address does change, I believe this
+ # XXX: limit/issue is present in vyatta too
+
+ if not options['local'] and options['dhcp-interface']:
+ # XXX: This behaviour changes from vyatta which would return 127.0.0.1 if
+ # XXX: the interface was not DHCP. As there is no easy way to find if an
+ # XXX: interface is using DHCP, and using this feature to get 127.0.0.1
+ # XXX: makes little sense, I feel the change in behaviour is acceptable
+ picked = get_interface_ip(options['dhcp-interface'])
+ if picked == '':
+ picked = '127.0.0.1'
+ print('Could not get an IP address from {dhcp-interface} using 127.0.0.1 instead')
+ options['local'] = picked
+ options['dhcp-interface'] = ''
+
+ # to make IPv6 SLAAC and DHCPv6 work with forwarding=1,
+ # accept_ra must be 2
+ if options['ipv6_autoconf'] or 'dhcpv6' in options['addresses-add']:
+ options['ipv6_accept_ra'] = 2
+
+ # allmulticast fate is linked to multicast
+ options['allmulticast'] = options['multicast']
+
+ # check that per encapsulation all local-remote pairs are unique
+ ct = conf.as_dict(['interfaces', 'tunnel'])
+ options['tunnel'] = {}
+
+ # check for bridges
+ options['bridge'] = is_member(config, ifname, 'bridge')
+ options['interfaces'] = interfaces()
+
+ for name in ct:
+ tunnel = ct[name]
+ encap = tunnel.get('encapsulation', '')
+ local = tunnel.get('local-ip', '')
+ if not local:
+ local = get_interface_ip(tunnel.get('dhcp-interface', ''))
+ remote = tunnel.get('remote-ip', '<unset>')
+ pair = f'{local}-{remote}'
+ options['tunnel'][encap][pair] = options['tunnel'].setdefault(encap, {}).get(pair, 0) + 1
+
+ return conf.to_api()
+
+
+def verify(conf):
+ options = conf['options']
+ changes = conf['changes']
+ actions = conf['actions']
+
+ ifname = options['ifname']
+ iftype = options['type']
+
+ if changes['section'] == 'delete':
+ if ifname in options['nhrp']:
+ raise ConfigError((
+ f'Cannot delete interface tunnel {iftype} {ifname}, '
+ 'it is used by NHRP'))
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Cannot delete interface "{options["ifname"]}" as it is a '
+ f'member of bridge "{options["bridge"]}"!'))
+
+ # done, bail out early
+ return None
+
+ # tunnel encapsulation checks
+
+ if not iftype:
+ raise ConfigError(f'Must provide an "encapsulation" for tunnel {iftype} {ifname}')
+
+ if changes['type'] in ('modify', 'delete'):
+ # TODO: we could now deal with encapsulation modification by deleting / recreating
+ raise ConfigError(f'Encapsulation can only be set at tunnel creation for tunnel {iftype} {ifname}')
+
+ if iftype != 'sit' and options['6rd-prefix']:
+ # XXX: should be able to remove this and let the definition catch it
+ print(f'6RD can only be configured for sit interfaces not tunnel {iftype} {ifname}')
+
+ # what are the tunnel options we can set / modified / deleted
+
+ kls = get_class(options)
+ valid = kls.updates + ['alias', 'addresses-add', 'addresses-del', 'vrf', 'state']
+ valid += ['arp_filter', 'arp_accept', 'arp_announce', 'arp_ignore']
+ valid += ['ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits']
+
+ if changes['section'] == 'create':
+ valid.extend(['type',])
+ valid.extend([o for o in kls.options if o not in kls.updates])
+
+ for create in actions['create']:
+ if create not in valid:
+ raise ConfigError(f'Can not set "{create}" for tunnel {iftype} {ifname} at tunnel creation')
+
+ for modify in actions['modify']:
+ if modify not in valid:
+ raise ConfigError(f'Can not modify "{modify}" for tunnel {iftype} {ifname}. it must be set at tunnel creation')
+
+ for delete in actions['delete']:
+ if delete in kls.required:
+ raise ConfigError(f'Can not remove "{delete}", it is an mandatory option for tunnel {iftype} {ifname}')
+
+ # tunnel information
+
+ tun_local = options['local']
+ afi_local = get_afi(tun_local)
+ tun_remote = options['remote'] or tun_local
+ afi_remote = get_afi(tun_remote)
+ tun_ismgre = iftype == 'gre' and not options['remote']
+ tun_is6rd = iftype == 'sit' and options['6rd-prefix']
+ tun_dev = options['dev']
+
+ # incompatible options
+
+ if not tun_local and not options['dhcp-interface'] and not tun_is6rd:
+ raise ConfigError(f'Must configure either local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+
+ if tun_local and options['dhcp-interface']:
+ raise ConfigError(f'Must configure only one of local-ip or dhcp-interface for tunnel {iftype} {ifname}')
+
+ if tun_dev and iftype in ('gre-bridge', 'sit'):
+ raise ConfigError(f'source interface can not be used with {iftype} {ifname}')
+
+ # tunnel endpoint
+
+ if afi_local != afi_remote:
+ raise ConfigError(f'IPv4/IPv6 mismatch between local-ip and remote-ip for tunnel {iftype} {ifname}')
+
+ if afi_local != kls.tunnel:
+ version = 4 if tun_local == IP4 else 6
+ raise ConfigError(f'Invalid IPv{version} local-ip for tunnel {iftype} {ifname}')
+
+ ipv4_count = len([ip for ip in options['addresses-add'] if is_ipv4(ip)])
+ ipv6_count = len([ip for ip in options['addresses-add'] if is_ipv6(ip)])
+
+ if tun_ismgre and afi_local == IP6:
+ raise ConfigError(f'Using an IPv6 address is forbidden for mGRE tunnels such as tunnel {iftype} {ifname}')
+
+ # check address family use
+ # checks are not enforced (but ip command failing) for backward compatibility
+
+ if ipv4_count and not IP4 in kls.ip:
+ print(f'Should not use IPv4 addresses on tunnel {iftype} {ifname}')
+
+ if ipv6_count and not IP6 in kls.ip:
+ print(f'Should not use IPv6 addresses on tunnel {iftype} {ifname}')
+
+ # vrf check
+ if options['vrf']:
+ if options['vrf'] not in options['interfaces']:
+ raise ConfigError(f'VRF "{options["vrf"]}" does not exist')
+
+ if options['bridge']:
+ raise ConfigError((
+ f'Interface "{options["ifname"]}" cannot be member of VRF '
+ f'"{options["vrf"]}" and bridge {options["bridge"]} '
+ f'at the same time!'))
+
+ # bridge and address check
+ if ( options['bridge']
+ and ( options['addresses-add']
+ or options['ipv6_autoconf'] ) ):
+ raise ConfigError((
+ f'Cannot assign address to interface "{options["name"]}" '
+ f'as it is a member of bridge "{options["bridge"]}"!'))
+
+ # source-interface check
+
+ if tun_dev and tun_dev not in options['interfaces']:
+ raise ConfigError(f'device "{tun_dev}" does not exist')
+
+ # tunnel encapsulation check
+
+ convert = {
+ (6, 4, 'gre'): 'ip6gre',
+ (6, 6, 'gre'): 'ip6gre',
+ (4, 6, 'ipip'): 'ipip6',
+ (6, 6, 'ipip'): 'ip6ip6',
+ }
+
+ iprotos = []
+ if ipv4_count:
+ iprotos.append(4)
+ if ipv6_count:
+ iprotos.append(6)
+
+ for iproto in iprotos:
+ replace = convert.get((kls.tunnel, iproto, iftype), '')
+ if replace:
+ raise ConfigError(
+ f'Using IPv6 address in local-ip or remote-ip is not possible with "encapsulation {iftype}". ' +
+ f'Use "encapsulation {replace}" for tunnel {iftype} {ifname} instead.'
+ )
+
+ # tunnel options
+
+ incompatible = []
+ if afi_local == IP6:
+ incompatible.extend(['ttl', 'tos', 'key',])
+ if afi_local == IP4:
+ incompatible.extend(['encaplimit', 'flowlabel', 'hoplimit', 'tclass'])
+
+ for option in incompatible:
+ if option in options:
+ # TODO: raise converted to print as not enforced by vyatta
+ # raise ConfigError(f'{option} is not valid for tunnel {iftype} {ifname}')
+ print(f'Using "{option}" is invalid for tunnel {iftype} {ifname}')
+
+ # duplicate tunnel pairs
+
+ pair = '{}-{}'.format(options['local'], options['remote'])
+ if options['tunnel'].get(iftype, {}).get(pair, 0) > 1:
+ raise ConfigError(f'More than one tunnel configured for with the same encapulation and IPs for tunnel {iftype} {ifname}')
+
+ return None
+
+
+def generate(gre):
+ return None
+
+def apply(conf):
+ options = conf['options']
+ changes = conf['changes']
+ actions = conf['actions']
+ kls = get_class(options)
+
+ # extract ifname as otherwise it is duplicated on the interface creation
+ ifname = options.pop('ifname')
+
+ # only the valid keys for creation of a Interface
+ config = dict((k, options[k]) for k in kls.options if options[k])
+
+ # setup or create the tunnel interface if it does not exist
+ tunnel = kls(ifname, **config)
+
+ if changes['section'] == 'delete':
+ tunnel.remove()
+ # The perl code was calling/opt/vyatta/sbin/vyatta-tunnel-cleanup
+ # which identified tunnels type which were not used anymore to remove them
+ # (ie: gre0, gretap0, etc.) The perl code did however nothing
+ # This feature is also not implemented yet
+ return
+
+ # A GRE interface without remote will be mGRE
+ # if the interface does not suppor the option, it skips the change
+ for option in tunnel.updates:
+ if changes['section'] in 'create' and option in tunnel.options:
+ # it was setup at creation
+ continue
+ if not options[option]:
+ # remote can be set to '' and it would generate an invalide command
+ continue
+ tunnel.set_interface(option, options[option])
+
+ # set other interface properties
+ for option in ('alias', 'mtu', 'link_detect', 'multicast', 'allmulticast',
+ 'arp_accept', 'arp_filter', 'arp_announce', 'arp_ignore',
+ 'ipv6_accept_ra', 'ipv6_autoconf', 'ipv6_forwarding', 'ipv6_dad_transmits'):
+ if not options[option]:
+ # should never happen but better safe
+ continue
+ tunnel.set_interface(option, options[option])
+
+ # assign/remove VRF (ONLY when not a member of a bridge,
+ # otherwise 'nomaster' removes it from it)
+ if not options['bridge']:
+ tunnel.set_vrf(options['vrf'])
+
+ # Configure interface address(es)
+ for addr in options['addresses-del']:
+ tunnel.del_addr(addr)
+ for addr in options['addresses-add']:
+ tunnel.add_addr(addr)
+
+ # now bring it up (or not)
+ tunnel.set_admin_state(options['state'])
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py
new file mode 100755
index 000000000..47c0bdcb8
--- /dev/null
+++ b/src/conf_mode/interfaces-vxlan.py
@@ -0,0 +1,120 @@
+#!/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_address
+from vyos.configverify import verify_bridge_delete
+from vyos.configverify import verify_source_interface
+from vyos.ifconfig import VXLANIf, Interface
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'vxlan']
+ vxlan = get_interface_dict(conf, base)
+
+ # VXLAN 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:
+ vxlan['mtu'] = '1450'
+
+ return vxlan
+
+def verify(vxlan):
+ if 'deleted' in vxlan:
+ verify_bridge_delete(vxlan)
+ return None
+
+ if int(vxlan['mtu']) < 1500:
+ print('WARNING: RFC7348 recommends VXLAN tunnels preserve a 1500 byte MTU')
+
+ if 'group' in vxlan:
+ if 'source_interface' not in vxlan:
+ raise ConfigError('Multicast VXLAN requires an underlaying interface ')
+
+ verify_source_interface(vxlan)
+
+ if not any(tmp in ['group', 'remote', 'source_address'] for tmp in vxlan):
+ raise ConfigError('Group, remote or source-address must be configured')
+
+ if 'vni' not in vxlan:
+ raise ConfigError('Must configure VNI for VXLAN')
+
+ if 'source_interface' in vxlan:
+ # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU
+ # if our configured MTU is at least 50 bytes less
+ underlay_mtu = int(Interface(vxlan['source_interface']).get_mtu())
+ if underlay_mtu < (int(vxlan['mtu']) + 50):
+ raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \
+ f'MTU is to small ({underlay_mtu} bytes)')
+
+ verify_address(vxlan)
+ return None
+
+
+def generate(vxlan):
+ return None
+
+
+def apply(vxlan):
+ # Check if the VXLAN interface already exists
+ if vxlan['ifname'] in interfaces():
+ v = VXLANIf(vxlan['ifname'])
+ # VXLAN is super picky and the tunnel always needs to be recreated,
+ # thus we can simply always delete it first.
+ v.remove()
+
+ if 'deleted' not in vxlan:
+ # VXLAN interface needs to be created on-block
+ # instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(VXLANIf.get_config())
+
+ # Assign VXLAN instance configuration parameters to config dict
+ for tmp in ['vni', 'group', 'source_address', 'source_interface', 'remote', 'port']:
+ if tmp in vxlan:
+ conf[tmp] = vxlan[tmp]
+
+ # Finally create the new interface
+ v = VXLANIf(vxlan['ifname'], **conf)
+ v.update(vxlan)
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py
new file mode 100755
index 000000000..8b64cde4d
--- /dev/null
+++ b/src/conf_mode/interfaces-wireguard.py
@@ -0,0 +1,115 @@
+#!/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 copy import deepcopy
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.configdict import get_interface_dict
+from vyos.configdict import node_changed
+from vyos.configdict import leaf_node_changed
+from vyos.configverify import verify_vrf
+from vyos.configverify import verify_address
+from vyos.configverify import verify_bridge_delete
+from vyos.ifconfig import WireGuardIf
+from vyos.util import check_kmod
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'wireguard']
+ wireguard = get_interface_dict(conf, base)
+
+ # Wireguard is "special" the default MTU is 1420 - 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:
+ wireguard['mtu'] = '1420'
+
+ # Mangle private key - it has a default so its always valid
+ wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard)
+
+ # Determine which Wireguard peer has been removed.
+ # Peers can only be removed with their public key!
+ tmp = node_changed(conf, ['peer'])
+ if tmp:
+ dict = {}
+ for peer in tmp:
+ peer_config = leaf_node_changed(conf, ['peer', peer, 'pubkey'])
+ dict = dict_merge({'peer_remove' : {peer : {'pubkey' : peer_config}}}, dict)
+ wireguard.update(dict)
+
+ return wireguard
+
+def verify(wireguard):
+ if 'deleted' in wireguard:
+ verify_bridge_delete(wireguard)
+ return None
+
+ verify_address(wireguard)
+ verify_vrf(wireguard)
+
+ if not os.path.exists(wireguard['private_key']):
+ raise ConfigError('Wireguard private-key not found! Execute: ' \
+ '"run generate wireguard [default-keypair|named-keypairs]"')
+
+ if 'address' not in wireguard:
+ raise ConfigError('IP address required!')
+
+ if 'peer' not in wireguard:
+ raise ConfigError('At least one Wireguard peer is required!')
+
+ # run checks on individual configured WireGuard peer
+ for tmp in wireguard['peer']:
+ peer = wireguard['peer'][tmp]
+
+ if 'allowed_ips' not in peer:
+ raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!')
+
+ if 'pubkey' not in peer:
+ raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!')
+
+ if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer):
+ raise ConfigError('Both Wireguard port and address must be defined '
+ f'for peer "{tmp}" if either one of them is set!')
+
+def apply(wireguard):
+ if 'deleted' in wireguard:
+ WireGuardIf(wireguard['ifname']).remove()
+ return None
+
+ w = WireGuardIf(wireguard['ifname'])
+ w.update(wireguard)
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod('wireguard')
+ c = get_config()
+ verify(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py
new file mode 100755
index 000000000..b6f247952
--- /dev/null
+++ b/src/conf_mode/interfaces-wireless.py
@@ -0,0 +1,260 @@
+#!/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 copy import deepcopy
+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 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():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ conf = Config()
+ base = ['interfaces', 'wireless']
+ wifi = get_interface_dict(conf, base)
+
+ 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)
+
+ # retrieve configured regulatory domain
+ conf.set_level(['system'])
+ if conf.exists(['wifi-regulatory-domain']):
+ wifi['country_code'] = conf.return_value(['wifi-regulatory-domain'])
+
+ # 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
+
+ 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 regulatory domain is mandatory,\n' \
+ 'use "set system wifi-regulatory-domain" for configuration.')
+
+ 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)
+
+ # 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)
+
+ elif wifi['type'] == 'station':
+ render(wpa_suppl_conf.format(**wifi), 'wifi/wpa_supplicant.conf.tmpl', wifi, trim_blocks=True)
+
+ return None
+
+def apply(wifi):
+ interface = wifi['ifname']
+ if 'deleted' in wifi:
+ WiFiIf(interface).remove()
+ else:
+ # WiFi interface needs to be created on-block (e.g. mode or physical
+ # interface) instead of passing a ton of arguments, I just use a dict
+ # that is managed by vyos.ifconfig
+ conf = deepcopy(WiFiIf.get_config())
+
+ # Assign WiFi instance configuration parameters to config dict
+ conf['phy'] = wifi['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
new file mode 100755
index 000000000..6d168d918
--- /dev/null
+++ b/src/conf_mode/interfaces-wirelessmodem.py
@@ -0,0 +1,127 @@
+#!/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():
+ """
+ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
+ interface name will be added or a deleted flag
+ """
+ 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
+ if not os.path.exists(find_device_file(wwan['device'])):
+ 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)
+ # Create PPP chat script
+ render(config_wwan_chat, 'wwan/chat.tmpl', wwan)
+
+ # generated script file must be executable
+
+ # Create script for ip-pre-up.d
+ render(script_wwan_pre_up, 'wwan/ip-pre-up.script.tmpl',
+ wwan, permission=0o755)
+ # Create script for ip-up.d
+ render(script_wwan_ip_up, 'wwan/ip-up.script.tmpl',
+ wwan, permission=0o755)
+ # Create script for ip-down.d
+ render(script_wwan_ip_down, 'wwan/ip-down.script.tmpl',
+ wwan, 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
new file mode 100755
index 000000000..015d1a480
--- /dev/null
+++ b/src/conf_mode/ipsec-settings.py
@@ -0,0 +1,227 @@
+#!/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 = 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)
+
+ 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)
+ # 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)
+ 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)
+ # 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/le_cert.py b/src/conf_mode/le_cert.py
new file mode 100755
index 000000000..755c89966
--- /dev/null
+++ b/src/conf_mode/le_cert.py
@@ -0,0 +1,115 @@
+#!/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
+import os
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+vyos_certbot_dir = vyos.defaults.directories['certbot']
+
+dependencies = [
+ 'https.py',
+]
+
+def request_certbot(cert):
+ email = cert.get('email')
+ if email is not None:
+ email_flag = '-m {0}'.format(email)
+ else:
+ email_flag = ''
+
+ domains = cert.get('domains')
+ if domains is not None:
+ domain_flag = '-d ' + ' -d '.join(domains)
+ else:
+ domain_flag = ''
+
+ certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}'
+
+ cmd(certbot_cmd,
+ raising=ConfigError,
+ message="The certbot request failed for the specified domains.")
+
+def get_config():
+ conf = Config()
+ if not conf.exists('service https certificates certbot'):
+ return None
+ else:
+ conf.set_level('service https certificates certbot')
+
+ cert = {}
+
+ if conf.exists('domain-name'):
+ cert['domains'] = conf.return_values('domain-name')
+
+ if conf.exists('email'):
+ cert['email'] = conf.return_value('email')
+
+ return cert
+
+def verify(cert):
+ if cert is None:
+ return None
+
+ if 'domains' not in cert:
+ raise ConfigError("At least one domain name is required to"
+ " request a letsencrypt certificate.")
+
+ if 'email' not in cert:
+ raise ConfigError("An email address is required to request"
+ " a letsencrypt certificate.")
+
+def generate(cert):
+ if cert is None:
+ return None
+
+ # certbot will attempt to reload nginx, even with 'certonly';
+ # start nginx if not active
+ ret = call('systemctl is-active --quiet nginx.service')
+ if ret:
+ call('systemctl start nginx.service')
+
+ request_certbot(cert)
+
+def apply(cert):
+ if cert is not None:
+ call('systemctl restart certbot.timer')
+ else:
+ call('systemctl stop certbot.timer')
+ return None
+
+ for dep in dependencies:
+ cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError)
+
+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/lldp.py b/src/conf_mode/lldp.py
new file mode 100755
index 000000000..1b539887a
--- /dev/null
+++ b/src/conf_mode/lldp.py
@@ -0,0 +1,249 @@
+#!/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
+import re
+
+from copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.validate import is_addr_assigned,is_loopback_addr
+from vyos.version import get_version_data
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = "/etc/default/lldpd"
+vyos_config_file = "/etc/lldpd.d/01-vyos.conf"
+base = ['service', 'lldp']
+
+default_config_data = {
+ "options": '',
+ "interface_list": '',
+ "location": ''
+}
+
+def get_options(config):
+ options = {}
+ config.set_level(base)
+
+ options['listen_vlan'] = config.exists('listen-vlan')
+ options['mgmt_addr'] = []
+ for addr in config.return_values('management-address'):
+ if is_addr_assigned(addr) and not is_loopback_addr(addr):
+ options['mgmt_addr'].append(addr)
+ else:
+ message = 'WARNING: LLDP management address {0} invalid - '.format(addr)
+ if is_loopback_addr(addr):
+ message += '(loopback address).'
+ else:
+ message += 'address not found.'
+ print(message)
+
+ snmp = config.exists('snmp enable')
+ options["snmp"] = snmp
+ if snmp:
+ config.set_level('')
+ options["sys_snmp"] = config.exists('service snmp')
+ config.set_level(base)
+
+ config.set_level(base + ['legacy-protocols'])
+ options['cdp'] = config.exists('cdp')
+ options['edp'] = config.exists('edp')
+ options['fdp'] = config.exists('fdp')
+ options['sonmp'] = config.exists('sonmp')
+
+ # start with an unknown version information
+ version_data = get_version_data()
+ options['description'] = version_data['version']
+ options['listen_on'] = []
+
+ return options
+
+def get_interface_list(config):
+ config.set_level(base)
+ intfs_names = config.list_nodes(['interface'])
+ if len(intfs_names) < 0:
+ return 0
+
+ interface_list = []
+ for name in intfs_names:
+ config.set_level(base + ['interface', name])
+ disable = config.exists(['disable'])
+ intf = {
+ 'name': name,
+ 'disable': disable
+ }
+ interface_list.append(intf)
+ return interface_list
+
+
+def get_location_intf(config, name):
+ path = base + ['interface', name]
+ config.set_level(path)
+
+ config.set_level(path + ['location'])
+ elin = ''
+ coordinate_based = {}
+
+ if config.exists('elin'):
+ elin = config.return_value('elin')
+
+ if config.exists('coordinate-based'):
+ config.set_level(path + ['location', 'coordinate-based'])
+
+ coordinate_based['latitude'] = config.return_value(['latitude'])
+ coordinate_based['longitude'] = config.return_value(['longitude'])
+
+ coordinate_based['altitude'] = '0'
+ if config.exists(['altitude']):
+ coordinate_based['altitude'] = config.return_value(['altitude'])
+
+ coordinate_based['datum'] = 'WGS84'
+ if config.exists(['datum']):
+ coordinate_based['datum'] = config.return_value(['datum'])
+
+ intf = {
+ 'name': name,
+ 'elin': elin,
+ 'coordinate_based': coordinate_based
+
+ }
+ return intf
+
+
+def get_location(config):
+ config.set_level(base)
+ intfs_names = config.list_nodes(['interface'])
+ if len(intfs_names) < 0:
+ return 0
+
+ if config.exists('disable'):
+ return 0
+
+ intfs_location = []
+ for name in intfs_names:
+ intf = get_location_intf(config, name)
+ intfs_location.append(intf)
+
+ return intfs_location
+
+
+def get_config():
+ lldp = deepcopy(default_config_data)
+ conf = Config()
+ if not conf.exists(base):
+ return None
+ else:
+ lldp['options'] = get_options(conf)
+ lldp['interface_list'] = get_interface_list(conf)
+ lldp['location'] = get_location(conf)
+
+ return lldp
+
+
+def verify(lldp):
+ # bail out early - looks like removal from running config
+ if lldp is None:
+ return
+
+ # check location
+ for location in lldp['location']:
+ # check coordinate-based
+ if len(location['coordinate_based']) > 0:
+ # check longitude and latitude
+ if not location['coordinate_based']['longitude']:
+ raise ConfigError('Must define longitude for interface {0}'.format(location['name']))
+
+ if not location['coordinate_based']['latitude']:
+ raise ConfigError('Must define latitude for interface {0}'.format(location['name']))
+
+ if not re.match(r'^(\d+)(\.\d+)?[nNsS]$', location['coordinate_based']['latitude']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'latitude should be a number followed by S or N'.format(location['name']))
+
+ if not re.match(r'^(\d+)(\.\d+)?[eEwW]$', location['coordinate_based']['longitude']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'longitude should be a number followed by E or W'.format(location['name']))
+
+ # check altitude and datum if exist
+ if location['coordinate_based']['altitude']:
+ if not re.match(r'^[-+0-9\.]+$', location['coordinate_based']['altitude']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'altitude should be a positive or negative number'.format(location['name']))
+
+ if location['coordinate_based']['datum']:
+ if not re.match(r'^(WGS84|NAD83|MLLW)$', location['coordinate_based']['datum']):
+ raise ConfigError("Invalid location for interface {0}:\n' \
+ 'datum should be WGS84, NAD83, or MLLW".format(location['name']))
+
+ # check elin
+ elif location['elin']:
+ if not re.match(r'^[0-9]{10,25}$', location['elin']):
+ raise ConfigError('Invalid location for interface {0}:\n' \
+ 'ELIN number must be between 10-25 numbers'.format(location['name']))
+
+ # check options
+ if lldp['options']['snmp']:
+ if not lldp['options']['sys_snmp']:
+ raise ConfigError('SNMP must be configured to enable LLDP SNMP')
+
+
+def generate(lldp):
+ # bail out early - looks like removal from running config
+ if lldp is None:
+ return
+
+ # generate listen on interfaces
+ for intf in lldp['interface_list']:
+ tmp = ''
+ # add exclamation mark if interface is disabled
+ if intf['disable']:
+ tmp = '!'
+
+ tmp += intf['name']
+ lldp['options']['listen_on'].append(tmp)
+
+ # generate /etc/default/lldpd
+ render(config_file, 'lldp/lldpd.tmpl', lldp)
+ # generate /etc/lldpd.d/01-vyos.conf
+ render(vyos_config_file, 'lldp/vyos.conf.tmpl', lldp)
+
+
+def apply(lldp):
+ if lldp:
+ # start/restart lldp service
+ call('systemctl restart lldpd.service')
+ else:
+ # LLDP service has been terminated
+ call('systemctl stop lldpd.service')
+ os.unlink(config_file)
+ os.unlink(vyos_config_file)
+
+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
new file mode 100755
index 000000000..dd34dfd66
--- /dev/null
+++ b/src/conf_mode/nat.py
@@ -0,0 +1,274 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import jmespath
+import json
+import os
+
+from copy import deepcopy
+from sys import exit
+from netifaces import interfaces
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+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()
+
+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():
+ nat = deepcopy(default_config_data)
+ conf = Config()
+
+ # read in current nftable (once) for further processing
+ tmp = cmd('nft -j list table raw')
+ nftable_json = json.loads(tmp)
+
+ # condense the full JSON table into a list with only relevand informations
+ pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
+ condensed_json = jmespath.search(pattern, nftable_json)
+
+ if not conf.exists(['nat']):
+ nat['helper_functions'] = 'remove'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
+
+ nat['deleted'] = True
+
+ return nat
+
+ # check if NAT connection tracking helpers need to be set up - this has to
+ # be done only once
+ if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
+ nat['helper_functions'] = 'add'
+
+ # Retrieve current table handler positions
+ nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
+ nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
+ nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
+ nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
+
+ # set config level for parsing in NAT configuration
+ conf.set_level(['nat'])
+
+ # use a common wrapper function to read in the source / destination
+ # tree from the config - thus we do not need to replicate almost the
+ # same code :-)
+ for tgt in ['source', 'destination', 'nptv6']:
+ nat[tgt] = parse_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' and not is_addr_assigned(addr):
+ print(f'Warning: IP address {addr} does not exist on the system!')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ for rule in nat['destination']:
+ interface = rule['interface_in']
+ err_msg = f'Destination NAT configuration error in rule "{rule["number"]}":'
+
+ if interface and interface not in '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')
+
+ # common rule verification
+ verify_rule(rule, err_msg)
+
+ return None
+
+def generate(nat):
+ render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat, trim_blocks=True, permission=0o755)
+ return None
+
+def apply(nat):
+ cmd(f'{iptables_nat_config}')
+ if os.path.isfile(iptables_nat_config):
+ os.unlink(iptables_nat_config)
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ check_kmod(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
new file mode 100755
index 000000000..bba8f87a4
--- /dev/null
+++ b/src/conf_mode/ntp.py
@@ -0,0 +1,82 @@
+#!/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():
+ 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)
+
+ 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_bfd.py b/src/conf_mode/protocols_bfd.py
new file mode 100755
index 000000000..c8e791c78
--- /dev/null
+++ b/src/conf_mode/protocols_bfd.py
@@ -0,0 +1,216 @@
+#!/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 vyos.config import Config
+from vyos.validate import is_ipv6_link_local, is_ipv6
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/bfd.frr'
+
+default_config_data = {
+ 'new_peers': [],
+ 'old_peers' : []
+}
+
+# get configuration for BFD peer from proposed or effective configuration
+def get_bfd_peer_config(peer, conf_mode="proposed"):
+ conf = Config()
+ conf.set_level('protocols bfd peer {0}'.format(peer))
+
+ bfd_peer = {
+ 'remote': peer,
+ 'shutdown': False,
+ 'src_if': '',
+ 'src_addr': '',
+ 'multiplier': '3',
+ 'rx_interval': '300',
+ 'tx_interval': '300',
+ 'multihop': False,
+ 'echo_interval': '',
+ 'echo_mode': False,
+ }
+
+ # Check if individual peer is disabled
+ if conf_mode == "effective" and conf.exists_effective('shutdown'):
+ bfd_peer['shutdown'] = True
+ if conf_mode == "proposed" and conf.exists('shutdown'):
+ bfd_peer['shutdown'] = True
+
+ # Check if peer has a local source interface configured
+ if conf_mode == "effective" and conf.exists_effective('source interface'):
+ bfd_peer['src_if'] = conf.return_effective_value('source interface')
+ if conf_mode == "proposed" and conf.exists('source interface'):
+ bfd_peer['src_if'] = conf.return_value('source interface')
+
+ # Check if peer has a local source address configured - this is mandatory for IPv6
+ if conf_mode == "effective" and conf.exists_effective('source address'):
+ bfd_peer['src_addr'] = conf.return_effective_value('source address')
+ if conf_mode == "proposed" and conf.exists('source address'):
+ bfd_peer['src_addr'] = conf.return_value('source address')
+
+ # Tell BFD daemon that we should expect packets with TTL less than 254
+ # (because it will take more than one hop) and to listen on the multihop
+ # port (4784)
+ if conf_mode == "effective" and conf.exists_effective('multihop'):
+ bfd_peer['multihop'] = True
+ if conf_mode == "proposed" and conf.exists('multihop'):
+ bfd_peer['multihop'] = True
+
+ # Configures the minimum interval that this system is capable of receiving
+ # control packets. The default value is 300 milliseconds.
+ if conf_mode == "effective" and conf.exists_effective('interval receive'):
+ bfd_peer['rx_interval'] = conf.return_effective_value('interval receive')
+ if conf_mode == "proposed" and conf.exists('interval receive'):
+ bfd_peer['rx_interval'] = conf.return_value('interval receive')
+
+ # The minimum transmission interval (less jitter) that this system wants
+ # to use to send BFD control packets.
+ if conf_mode == "effective" and conf.exists_effective('interval transmit'):
+ bfd_peer['tx_interval'] = conf.return_effective_value('interval transmit')
+ if conf_mode == "proposed" and conf.exists('interval transmit'):
+ bfd_peer['tx_interval'] = conf.return_value('interval transmit')
+
+ # Configures the detection multiplier to determine packet loss. The remote
+ # transmission interval will be multiplied by this value to determine the
+ # connection loss detection timer. The default value is 3.
+ if conf_mode == "effective" and conf.exists_effective('interval multiplier'):
+ bfd_peer['multiplier'] = conf.return_effective_value('interval multiplier')
+ if conf_mode == "proposed" and conf.exists('interval multiplier'):
+ bfd_peer['multiplier'] = conf.return_value('interval multiplier')
+
+ # Configures the minimal echo receive transmission interval that this system is capable of handling
+ if conf_mode == "effective" and conf.exists_effective('interval echo-interval'):
+ bfd_peer['echo_interval'] = conf.return_effective_value('interval echo-interval')
+ if conf_mode == "proposed" and conf.exists('interval echo-interval'):
+ bfd_peer['echo_interval'] = conf.return_value('interval echo-interval')
+
+ # Enables or disables the echo transmission mode
+ if conf_mode == "effective" and conf.exists_effective('echo-mode'):
+ bfd_peer['echo_mode'] = True
+ if conf_mode == "proposed" and conf.exists('echo-mode'):
+ bfd_peer['echo_mode'] = True
+
+ return bfd_peer
+
+def get_config():
+ bfd = deepcopy(default_config_data)
+ conf = Config()
+ if not (conf.exists('protocols bfd') or conf.exists_effective('protocols bfd')):
+ return None
+ else:
+ conf.set_level('protocols bfd')
+
+ # as we have to use vtysh to talk to FRR we also need to know
+ # which peers are gone due to a config removal - thus we read in
+ # all peers (active or to delete)
+ for peer in conf.list_effective_nodes('peer'):
+ bfd['old_peers'].append(get_bfd_peer_config(peer, "effective"))
+
+ for peer in conf.list_nodes('peer'):
+ bfd['new_peers'].append(get_bfd_peer_config(peer))
+
+ # find deleted peers
+ set_new_peers = set(conf.list_nodes('peer'))
+ set_old_peers = set(conf.list_effective_nodes('peer'))
+ bfd['deleted_peers'] = set_old_peers - set_new_peers
+
+ return bfd
+
+def verify(bfd):
+ if bfd is None:
+ return None
+
+ # some variables to use later
+ conf = Config()
+
+ for peer in bfd['new_peers']:
+ # IPv6 link local peers require an explicit local address/interface
+ if is_ipv6_link_local(peer['remote']):
+ if not (peer['src_if'] and peer['src_addr']):
+ raise ConfigError('BFD IPv6 link-local peers require explicit local address and interface setting')
+
+ # IPv6 peers require an explicit local address
+ if is_ipv6(peer['remote']):
+ if not peer['src_addr']:
+ raise ConfigError('BFD IPv6 peers require explicit local address setting')
+
+ # multihop require source address
+ if peer['multihop'] and not peer['src_addr']:
+ raise ConfigError('Multihop require source address')
+
+ # multihop and echo-mode cannot be used together
+ if peer['multihop'] and peer['echo_mode']:
+ raise ConfigError('Multihop and echo-mode cannot be used together')
+
+ # multihop doesn't accept interface names
+ if peer['multihop'] and peer['src_if']:
+ raise ConfigError('Multihop and source interface cannot be used together')
+
+ # echo interval can be configured only with enabled echo-mode
+ if peer['echo_interval'] != '' and not peer['echo_mode']:
+ raise ConfigError('echo-interval can be configured only with enabled echo-mode')
+
+ # check if we deleted peers are not used in configuration
+ if conf.exists('protocols bgp'):
+ bgp_as = conf.list_nodes('protocols bgp')[0]
+
+ # check BGP neighbors
+ for peer in bfd['deleted_peers']:
+ if conf.exists('protocols bgp {0} neighbor {1} bfd'.format(bgp_as, peer)):
+ raise ConfigError('Cannot delete BFD peer {0}: it is used in BGP configuration'.format(peer))
+ if conf.exists('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer)):
+ peer_group = conf.return_value('protocols bgp {0} neighbor {1} peer-group'.format(bgp_as, peer))
+ if conf.exists('protocols bgp {0} peer-group {1} bfd'.format(bgp_as, peer_group)):
+ raise ConfigError('Cannot delete BFD peer {0}: it belongs to BGP peer-group {1} with enabled BFD'.format(peer, peer_group))
+
+ return None
+
+def generate(bfd):
+ if bfd is None:
+ return None
+
+ render(config_file, 'frr/bfd.frr.tmpl', bfd)
+ return None
+
+def apply(bfd):
+ if bfd is None:
+ return None
+
+ call("vtysh -d bfdd -f " + config_file)
+ if os.path.exists(config_file):
+ os.remove(config_file)
+
+ 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
new file mode 100755
index 000000000..3aa76d866
--- /dev/null
+++ b/src/conf_mode/protocols_bgp.py
@@ -0,0 +1,102 @@
+#!/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
+
+from copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos.template import render
+from vyos import ConfigError, airbag
+airbag.enable()
+
+config_file = r'/tmp/bgp.frr'
+
+default_config_data = {
+ 'as_number': ''
+}
+
+def get_config():
+ bgp = deepcopy(default_config_data)
+ conf = Config()
+
+ # this lives in the "nbgp" tree until we switch over
+ base = ['protocols', 'nbgp']
+ if not conf.exists(base):
+ return None
+
+ bgp = deepcopy(default_config_data)
+ # Get full BGP configuration as dictionary - output the configuration for development
+ #
+ # vyos@vyos# commit
+ # [ protocols nbgp 65000 ]
+ # {'nbgp': {'65000': {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {},
+ # '2.2.2.0/24': {}}},
+ # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}},
+ # 'neighbor': {'192.0.2.1': {'password': 'foo',
+ # 'remote-as': '100'}}}}}
+ #
+ tmp = conf.get_config_dict(base)
+
+ # extract base key from dict as this is our AS number
+ bgp['as_number'] = jmespath.search('nbgp | keys(@) [0]', tmp)
+
+ # adjust level of dictionary returned by get_config_dict()
+ # by using jmesgpath and update dictionary
+ bgp.update(jmespath.search('nbgp.* | [0]', tmp))
+
+ from pprint import pprint
+ pprint(bgp)
+ # resulting in e.g.
+ # vyos@vyos# commit
+ # [ protocols nbgp 65000 ]
+ # {'address-family': {'ipv4-unicast': {'aggregate-address': {'1.1.0.0/16': {},
+ # '2.2.2.0/24': {}}},
+ # 'ipv6-unicast': {'aggregate-address': {'2001:db8::/32': {}}}},
+ # 'as_number': '65000',
+ # 'neighbor': {'192.0.2.1': {'password': 'foo', 'remote-as': '100'}},
+ # 'timers': {'holdtime': '5'}}
+
+ return bgp
+
+def verify(bgp):
+ # bail out early - looks like removal from running config
+ if not bgp:
+ return None
+
+ return None
+
+def generate(bgp):
+ # bail out early - looks like removal from running config
+ if not bgp:
+ return None
+
+ render(config_file, 'frr/bgp.frr.tmpl', bgp)
+ return None
+
+def apply(bgp):
+ 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_igmp.py b/src/conf_mode/protocols_igmp.py
new file mode 100755
index 000000000..ca148fd6a
--- /dev/null
+++ b/src/conf_mode/protocols_igmp.py
@@ -0,0 +1,113 @@
+#!/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 ipaddress import IPv4Address
+from sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/igmp.frr'
+
+def get_config():
+ conf = Config()
+ igmp_conf = {
+ 'igmp_conf' : False,
+ 'old_ifaces' : {},
+ 'ifaces' : {}
+ }
+ if not (conf.exists('protocols igmp') or conf.exists_effective('protocols igmp')):
+ return None
+
+ if conf.exists('protocols igmp'):
+ igmp_conf['igmp_conf'] = True
+
+ conf.set_level('protocols igmp')
+
+ # # Get interfaces
+ for iface in conf.list_effective_nodes('interface'):
+ igmp_conf['old_ifaces'].update({
+ iface : {
+ 'version' : conf.return_effective_value('interface {0} version'.format(iface)),
+ 'query_interval' : conf.return_effective_value('interface {0} query-interval'.format(iface)),
+ 'query_max_resp_time' : conf.return_effective_value('interface {0} query-max-response-time'.format(iface)),
+ 'gr_join' : {}
+ }
+ })
+ for gr_join in conf.list_effective_nodes('interface {0} join'.format(iface)):
+ igmp_conf['old_ifaces'][iface]['gr_join'][gr_join] = conf.return_effective_values('interface {0} join {1} source'.format(iface, gr_join))
+
+ for iface in conf.list_nodes('interface'):
+ igmp_conf['ifaces'].update({
+ iface : {
+ 'version' : conf.return_value('interface {0} version'.format(iface)),
+ 'query_interval' : conf.return_value('interface {0} query-interval'.format(iface)),
+ 'query_max_resp_time' : conf.return_value('interface {0} query-max-response-time'.format(iface)),
+ 'gr_join' : {}
+ }
+ })
+ for gr_join in conf.list_nodes('interface {0} join'.format(iface)):
+ igmp_conf['ifaces'][iface]['gr_join'][gr_join] = conf.return_values('interface {0} join {1} source'.format(iface, gr_join))
+
+ return igmp_conf
+
+def verify(igmp):
+ if igmp is None:
+ return None
+
+ if igmp['igmp_conf']:
+ # Check interfaces
+ if not igmp['ifaces']:
+ raise ConfigError(f"IGMP require defined interfaces!")
+ # Check, is this multicast group
+ for intfc in igmp['ifaces']:
+ for gr_addr in igmp['ifaces'][intfc]['gr_join']:
+ if IPv4Address(gr_addr) < IPv4Address('224.0.0.0'):
+ raise ConfigError(gr_addr + " not a multicast group")
+
+def generate(igmp):
+ if igmp is None:
+ return None
+
+ render(config_file, 'frr/igmp.frr.tmpl', igmp)
+ return None
+
+def apply(igmp):
+ if igmp is None:
+ return None
+
+ if os.path.exists(config_file):
+ call(f'vtysh -d pimd -f {config_file}')
+ os.remove(config_file)
+
+ 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
new file mode 100755
index 000000000..bcb16fa04
--- /dev/null
+++ b/src/conf_mode/protocols_mpls.py
@@ -0,0 +1,187 @@
+#!/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 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'/tmp/ldpd.frr'
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ conf = Config()
+ mpls_conf = {
+ 'router_id' : None,
+ 'mpls_ldp' : False,
+ 'old_ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None,
+ 'hello_holdtime' : None,
+ 'hello_interval' : None
+ },
+ 'ldp' : {
+ 'interfaces' : [],
+ 'neighbors' : {},
+ 'd_transp_ipv4' : None,
+ 'd_transp_ipv6' : None,
+ 'hello_holdtime' : None,
+ 'hello_interval' : None
+ }
+ }
+ if not (conf.exists('protocols mpls') or conf.exists_effective('protocols mpls')):
+ return None
+
+ if conf.exists('protocols mpls ldp'):
+ mpls_conf['mpls_ldp'] = True
+
+ conf.set_level('protocols mpls ldp')
+
+ # Get router-id
+ if conf.exists_effective('router-id'):
+ mpls_conf['old_router_id'] = conf.return_effective_value('router-id')
+ if conf.exists('router-id'):
+ mpls_conf['router_id'] = conf.return_value('router-id')
+
+ # Get hello holdtime
+ if conf.exists_effective('discovery hello-holdtime'):
+ mpls_conf['old_ldp']['hello_holdtime'] = conf.return_effective_value('discovery hello-holdtime')
+
+ if conf.exists('discovery hello-holdtime'):
+ mpls_conf['ldp']['hello_holdtime'] = conf.return_value('discovery hello-holdtime')
+
+ # Get hello interval
+ if conf.exists_effective('discovery hello-interval'):
+ mpls_conf['old_ldp']['hello_interval'] = conf.return_effective_value('discovery hello-interval')
+
+ if conf.exists('discovery hello-interval'):
+ mpls_conf['ldp']['hello_interval'] = conf.return_value('discovery hello-interval')
+
+ # Get discovery transport-ipv4-address
+ if conf.exists_effective('discovery transport-ipv4-address'):
+ mpls_conf['old_ldp']['d_transp_ipv4'] = conf.return_effective_value('discovery transport-ipv4-address')
+
+ if conf.exists('discovery transport-ipv4-address'):
+ mpls_conf['ldp']['d_transp_ipv4'] = conf.return_value('discovery transport-ipv4-address')
+
+ # Get discovery transport-ipv6-address
+ if conf.exists_effective('discovery transport-ipv6-address'):
+ mpls_conf['old_ldp']['d_transp_ipv6'] = conf.return_effective_value('discovery transport-ipv6-address')
+
+ if conf.exists('discovery transport-ipv6-address'):
+ mpls_conf['ldp']['d_transp_ipv6'] = conf.return_value('discovery transport-ipv6-address')
+
+ # Get interfaces
+ if conf.exists_effective('interface'):
+ mpls_conf['old_ldp']['interfaces'] = conf.return_effective_values('interface')
+
+ if conf.exists('interface'):
+ mpls_conf['ldp']['interfaces'] = conf.return_values('interface')
+
+ # Get neighbors
+ for neighbor in conf.list_effective_nodes('neighbor'):
+ mpls_conf['old_ldp']['neighbors'].update({
+ neighbor : {
+ 'password' : conf.return_effective_value('neighbor {0} password'.format(neighbor))
+ }
+ })
+
+ for neighbor in conf.list_nodes('neighbor'):
+ mpls_conf['ldp']['neighbors'].update({
+ neighbor : {
+ 'password' : conf.return_value('neighbor {0} password'.format(neighbor))
+ }
+ })
+
+ return mpls_conf
+
+def operate_mpls_on_intfc(interfaces, action):
+ rp_filter = 0
+ if action == 1:
+ rp_filter = 2
+ for iface in interfaces:
+ sysctl('net.mpls.conf.{0}.input'.format(iface), action)
+ # Operate rp filter
+ sysctl('net.ipv4.conf.{0}.rp_filter'.format(iface), rp_filter)
+
+def verify(mpls):
+ if mpls is None:
+ return None
+
+ if mpls['mpls_ldp']:
+ # Requre router-id
+ if not mpls['router_id']:
+ raise ConfigError(f"MPLS ldp router-id is mandatory!")
+
+ # Requre discovery transport-address
+ if not mpls['ldp']['d_transp_ipv4'] and not mpls['ldp']['d_transp_ipv6']:
+ raise ConfigError(f"MPLS ldp discovery transport address is mandatory!")
+
+ # Requre interface
+ if not mpls['ldp']['interfaces']:
+ raise ConfigError(f"MPLS ldp interface is mandatory!")
+
+def generate(mpls):
+ if mpls is None:
+ return None
+
+ render(config_file, 'frr/ldpd.frr.tmpl', mpls)
+ return None
+
+def apply(mpls):
+ if mpls is None:
+ return None
+
+ # Set number of entries in the platform label table
+ if mpls['mpls_ldp']:
+ sysctl('net.mpls.platform_labels', '1048575')
+ else:
+ sysctl('net.mpls.platform_labels', '0')
+
+ # Do not copy IP TTL to MPLS header
+ sysctl('net.mpls.ip_ttl_propagate', '0')
+
+ # Allow mpls on interfaces
+ operate_mpls_on_intfc(mpls['ldp']['interfaces'], 1)
+
+ # Disable mpls on deleted interfaces
+ diactive_ifaces = set(mpls['old_ldp']['interfaces']).difference(mpls['ldp']['interfaces'])
+ operate_mpls_on_intfc(diactive_ifaces, 0)
+
+ if os.path.exists(config_file):
+ call(f'vtysh -d ldpd -f {config_file}')
+ os.remove(config_file)
+
+ 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_pim.py b/src/conf_mode/protocols_pim.py
new file mode 100755
index 000000000..8aa324bac
--- /dev/null
+++ b/src/conf_mode/protocols_pim.py
@@ -0,0 +1,140 @@
+#!/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 ipaddress import IPv4Address
+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'/tmp/pimd.frr'
+
+def get_config():
+ conf = Config()
+ pim_conf = {
+ 'pim_conf' : False,
+ 'old_pim' : {
+ 'ifaces' : {},
+ 'rp' : {}
+ },
+ 'pim' : {
+ 'ifaces' : {},
+ 'rp' : {}
+ }
+ }
+ if not (conf.exists('protocols pim') or conf.exists_effective('protocols pim')):
+ return None
+
+ if conf.exists('protocols pim'):
+ pim_conf['pim_conf'] = True
+
+ conf.set_level('protocols pim')
+
+ # Get interfaces
+ for iface in conf.list_effective_nodes('interface'):
+ pim_conf['old_pim']['ifaces'].update({
+ iface : {
+ 'hello' : conf.return_effective_value('interface {0} hello'.format(iface)),
+ 'dr_prio' : conf.return_effective_value('interface {0} dr-priority'.format(iface))
+ }
+ })
+
+ for iface in conf.list_nodes('interface'):
+ pim_conf['pim']['ifaces'].update({
+ iface : {
+ 'hello' : conf.return_value('interface {0} hello'.format(iface)),
+ 'dr_prio' : conf.return_value('interface {0} dr-priority'.format(iface)),
+ }
+ })
+
+ conf.set_level('protocols pim rp')
+
+ # Get RPs addresses
+ for rp_addr in conf.list_effective_nodes('address'):
+ pim_conf['old_pim']['rp'][rp_addr] = conf.return_effective_values('address {0} group'.format(rp_addr))
+
+ for rp_addr in conf.list_nodes('address'):
+ pim_conf['pim']['rp'][rp_addr] = conf.return_values('address {0} group'.format(rp_addr))
+
+ # Get RP keep-alive-timer
+ if conf.exists_effective('rp keep-alive-timer'):
+ pim_conf['old_pim']['rp_keep_alive'] = conf.return_effective_value('rp keep-alive-timer')
+ if conf.exists('rp keep-alive-timer'):
+ pim_conf['pim']['rp_keep_alive'] = conf.return_value('rp keep-alive-timer')
+
+ return pim_conf
+
+def verify(pim):
+ if pim is None:
+ return None
+
+ if pim['pim_conf']:
+ # Check interfaces
+ if not pim['pim']['ifaces']:
+ raise ConfigError(f"PIM require defined interfaces!")
+
+ if not pim['pim']['rp']:
+ raise ConfigError(f"RP address required")
+
+ # Check unique multicast groups
+ uniq_groups = []
+ for rp_addr in pim['pim']['rp']:
+ if not pim['pim']['rp'][rp_addr]:
+ raise ConfigError(f"Group should be specified for RP " + rp_addr)
+ for group in pim['pim']['rp'][rp_addr]:
+ if (group in uniq_groups):
+ raise ConfigError(f"Group range " + group + " specified cannot exact match another")
+
+ # Check, is this multicast group
+ gr_addr = group.split('/')
+ if IPv4Address(gr_addr[0]) < IPv4Address('224.0.0.0'):
+ raise ConfigError(group + " not a multicast group")
+
+ uniq_groups.extend(pim['pim']['rp'][rp_addr])
+
+def generate(pim):
+ if pim is None:
+ return None
+
+ render(config_file, 'frr/pimd.frr.tmpl', pim)
+ return None
+
+def apply(pim):
+ if pim is None:
+ return None
+
+ if os.path.exists(config_file):
+ call("vtysh -d pimd -f " + config_file)
+ os.remove(config_file)
+
+ 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_rip.py b/src/conf_mode/protocols_rip.py
new file mode 100755
index 000000000..4f8816d61
--- /dev/null
+++ b/src/conf_mode/protocols_rip.py
@@ -0,0 +1,317 @@
+#!/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 import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/ripd.frr'
+
+def get_config():
+ conf = Config()
+ base = ['protocols', 'rip']
+ rip_conf = {
+ 'rip_conf' : False,
+ 'default_distance' : [],
+ 'default_originate' : False,
+ 'old_rip' : {
+ 'default_metric' : [],
+ 'distribute' : {},
+ 'neighbors' : {},
+ 'networks' : {},
+ 'net_distance' : {},
+ 'passive_iface' : {},
+ 'redist' : {},
+ 'route' : {},
+ 'ifaces' : {},
+ 'timer_garbage' : 120,
+ 'timer_timeout' : 180,
+ 'timer_update' : 30
+ },
+ 'rip' : {
+ 'default_metric' : None,
+ 'distribute' : {},
+ 'neighbors' : {},
+ 'networks' : {},
+ 'net_distance' : {},
+ 'passive_iface' : {},
+ 'redist' : {},
+ 'route' : {},
+ 'ifaces' : {},
+ 'timer_garbage' : 120,
+ 'timer_timeout' : 180,
+ 'timer_update' : 30
+ }
+ }
+
+ if not (conf.exists(base) or conf.exists_effective(base)):
+ return None
+
+ if conf.exists(base):
+ rip_conf['rip_conf'] = True
+
+ conf.set_level(base)
+
+ # Get default distance
+ if conf.exists_effective('default-distance'):
+ rip_conf['old_default_distance'] = conf.return_effective_value('default-distance')
+
+ if conf.exists('default-distance'):
+ rip_conf['default_distance'] = conf.return_value('default-distance')
+
+ # Get default information originate (originate default route)
+ if conf.exists_effective('default-information originate'):
+ rip_conf['old_default_originate'] = True
+
+ if conf.exists('default-information originate'):
+ rip_conf['default_originate'] = True
+
+ # Get default-metric
+ if conf.exists_effective('default-metric'):
+ rip_conf['old_rip']['default_metric'] = conf.return_effective_value('default-metric')
+
+ if conf.exists('default-metric'):
+ rip_conf['rip']['default_metric'] = conf.return_value('default-metric')
+
+ # Get distribute list interface old_rip
+ for dist_iface in conf.list_effective_nodes('distribute-list interface'):
+ # Set level 'distribute-list interface ethX'
+ conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface)
+ rip_conf['rip']['distribute'].update({
+ dist_iface : {
+ 'iface_access_list_in': conf.return_effective_value('access-list in'.format(dist_iface)),
+ 'iface_access_list_out': conf.return_effective_value('access-list out'.format(dist_iface)),
+ 'iface_prefix_list_in': conf.return_effective_value('prefix-list in'.format(dist_iface)),
+ 'iface_prefix_list_out': conf.return_effective_value('prefix-list out'.format(dist_iface))
+ }
+ })
+
+ # Access-list in old_rip
+ if conf.exists_effective('access-list in'.format(dist_iface)):
+ rip_conf['old_rip']['iface_access_list_in'] = conf.return_effective_value('access-list in'.format(dist_iface))
+ # Access-list out old_rip
+ if conf.exists_effective('access-list out'.format(dist_iface)):
+ rip_conf['old_rip']['iface_access_list_out'] = conf.return_effective_value('access-list out'.format(dist_iface))
+ # Prefix-list in old_rip
+ if conf.exists_effective('prefix-list in'.format(dist_iface)):
+ rip_conf['old_rip']['iface_prefix_list_in'] = conf.return_effective_value('prefix-list in'.format(dist_iface))
+ # Prefix-list out old_rip
+ if conf.exists_effective('prefix-list out'.format(dist_iface)):
+ rip_conf['old_rip']['iface_prefix_list_out'] = conf.return_effective_value('prefix-list out'.format(dist_iface))
+
+ conf.set_level(base)
+
+ # Get distribute list interface
+ for dist_iface in conf.list_nodes('distribute-list interface'):
+ # Set level 'distribute-list interface ethX'
+ conf.set_level((str(base)) + ' distribute-list interface ' + dist_iface)
+ rip_conf['rip']['distribute'].update({
+ dist_iface : {
+ 'iface_access_list_in': conf.return_value('access-list in'.format(dist_iface)),
+ 'iface_access_list_out': conf.return_value('access-list out'.format(dist_iface)),
+ 'iface_prefix_list_in': conf.return_value('prefix-list in'.format(dist_iface)),
+ 'iface_prefix_list_out': conf.return_value('prefix-list out'.format(dist_iface))
+ }
+ })
+
+ # Access-list in
+ if conf.exists('access-list in'.format(dist_iface)):
+ rip_conf['rip']['iface_access_list_in'] = conf.return_value('access-list in'.format(dist_iface))
+ # Access-list out
+ if conf.exists('access-list out'.format(dist_iface)):
+ rip_conf['rip']['iface_access_list_out'] = conf.return_value('access-list out'.format(dist_iface))
+ # Prefix-list in
+ if conf.exists('prefix-list in'.format(dist_iface)):
+ rip_conf['rip']['iface_prefix_list_in'] = conf.return_value('prefix-list in'.format(dist_iface))
+ # Prefix-list out
+ if conf.exists('prefix-list out'.format(dist_iface)):
+ rip_conf['rip']['iface_prefix_list_out'] = conf.return_value('prefix-list out'.format(dist_iface))
+
+ conf.set_level((str(base)) + ' distribute-list')
+
+ # Get distribute list, access-list in
+ if conf.exists_effective('access-list in'):
+ rip_conf['old_rip']['dist_acl_in'] = conf.return_effective_value('access-list in')
+
+ if conf.exists('access-list in'):
+ rip_conf['rip']['dist_acl_in'] = conf.return_value('access-list in')
+
+ # Get distribute list, access-list out
+ if conf.exists_effective('access-list out'):
+ rip_conf['old_rip']['dist_acl_out'] = conf.return_effective_value('access-list out')
+
+ if conf.exists('access-list out'):
+ rip_conf['rip']['dist_acl_out'] = conf.return_value('access-list out')
+
+ # Get ditstribute list, prefix-list in
+ if conf.exists_effective('prefix-list in'):
+ rip_conf['old_rip']['dist_prfx_in'] = conf.return_effective_value('prefix-list in')
+
+ if conf.exists('prefix-list in'):
+ rip_conf['rip']['dist_prfx_in'] = conf.return_value('prefix-list in')
+
+ # Get distribute list, prefix-list out
+ if conf.exists_effective('prefix-list out'):
+ rip_conf['old_rip']['dist_prfx_out'] = conf.return_effective_value('prefix-list out')
+
+ if conf.exists('prefix-list out'):
+ rip_conf['rip']['dist_prfx_out'] = conf.return_value('prefix-list out')
+
+ conf.set_level(base)
+
+ # Get network Interfaces
+ if conf.exists_effective('interface'):
+ rip_conf['old_rip']['ifaces'] = conf.return_effective_values('interface')
+
+ if conf.exists('interface'):
+ rip_conf['rip']['ifaces'] = conf.return_values('interface')
+
+ # Get neighbors
+ if conf.exists_effective('neighbor'):
+ rip_conf['old_rip']['neighbors'] = conf.return_effective_values('neighbor')
+
+ if conf.exists('neighbor'):
+ rip_conf['rip']['neighbors'] = conf.return_values('neighbor')
+
+ # Get networks
+ if conf.exists_effective('network'):
+ rip_conf['old_rip']['networks'] = conf.return_effective_values('network')
+
+ if conf.exists('network'):
+ rip_conf['rip']['networks'] = conf.return_values('network')
+
+ # Get network-distance old_rip
+ for net_dist in conf.list_effective_nodes('network-distance'):
+ rip_conf['old_rip']['net_distance'].update({
+ net_dist : {
+ 'access_list' : conf.return_effective_value('network-distance {0} access-list'.format(net_dist)),
+ 'distance' : conf.return_effective_value('network-distance {0} distance'.format(net_dist)),
+ }
+ })
+
+ # Get network-distance
+ for net_dist in conf.list_nodes('network-distance'):
+ rip_conf['rip']['net_distance'].update({
+ net_dist : {
+ 'access_list' : conf.return_value('network-distance {0} access-list'.format(net_dist)),
+ 'distance' : conf.return_value('network-distance {0} distance'.format(net_dist)),
+ }
+ })
+
+ # Get passive-interface
+ if conf.exists_effective('passive-interface'):
+ rip_conf['old_rip']['passive_iface'] = conf.return_effective_values('passive-interface')
+
+ if conf.exists('passive-interface'):
+ rip_conf['rip']['passive_iface'] = conf.return_values('passive-interface')
+
+ # Get redistribute for old_rip
+ for protocol in conf.list_effective_nodes('redistribute'):
+ rip_conf['old_rip']['redist'].update({
+ protocol : {
+ 'metric' : conf.return_effective_value('redistribute {0} metric'.format(protocol)),
+ 'route_map' : conf.return_effective_value('redistribute {0} route-map'.format(protocol)),
+ }
+ })
+
+ # Get redistribute
+ for protocol in conf.list_nodes('redistribute'):
+ rip_conf['rip']['redist'].update({
+ protocol : {
+ 'metric' : conf.return_value('redistribute {0} metric'.format(protocol)),
+ 'route_map' : conf.return_value('redistribute {0} route-map'.format(protocol)),
+ }
+ })
+
+ conf.set_level(base)
+
+ # Get route
+ if conf.exists_effective('route'):
+ rip_conf['old_rip']['route'] = conf.return_effective_values('route')
+
+ if conf.exists('route'):
+ rip_conf['rip']['route'] = conf.return_values('route')
+
+ # Get timers garbage
+ if conf.exists_effective('timers garbage-collection'):
+ rip_conf['old_rip']['timer_garbage'] = conf.return_effective_value('timers garbage-collection')
+
+ if conf.exists('timers garbage-collection'):
+ rip_conf['rip']['timer_garbage'] = conf.return_value('timers garbage-collection')
+
+ # Get timers timeout
+ if conf.exists_effective('timers timeout'):
+ rip_conf['old_rip']['timer_timeout'] = conf.return_effective_value('timers timeout')
+
+ if conf.exists('timers timeout'):
+ rip_conf['rip']['timer_timeout'] = conf.return_value('timers timeout')
+
+ # Get timers update
+ if conf.exists_effective('timers update'):
+ rip_conf['old_rip']['timer_update'] = conf.return_effective_value('timers update')
+
+ if conf.exists('timers update'):
+ rip_conf['rip']['timer_update'] = conf.return_value('timers update')
+
+ return rip_conf
+
+def verify(rip):
+ if rip is None:
+ return None
+
+ # Check for network. If network-distance acl is set and distance not set
+ for net in rip['rip']['net_distance']:
+ if not rip['rip']['net_distance'][net]['distance']:
+ raise ConfigError(f"Must specify distance for network {net}")
+
+def generate(rip):
+ if rip is None:
+ return None
+
+ render(config_file, 'frr/rip.frr.tmpl', rip)
+ return None
+
+def apply(rip):
+ if rip is None:
+ return None
+
+ if os.path.exists(config_file):
+ call(f'vtysh -d ripd -f {config_file}')
+ os.remove(config_file)
+ else:
+ print("File {0} not found".format(config_file))
+
+
+ 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_static_multicast.py b/src/conf_mode/protocols_static_multicast.py
new file mode 100755
index 000000000..232d1e181
--- /dev/null
+++ b/src/conf_mode/protocols_static_multicast.py
@@ -0,0 +1,117 @@
+#!/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 ipaddress import IPv4Address
+from sys import exit
+
+from vyos import ConfigError
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/tmp/static_mcast.frr'
+
+# Get configuration for static multicast route
+def get_config():
+ conf = Config()
+ mroute = {
+ 'old_mroute' : {},
+ 'mroute' : {}
+ }
+
+ base_path = "protocols static multicast"
+
+ if not (conf.exists(base_path) or conf.exists_effective(base_path)):
+ return None
+
+ conf.set_level(base_path)
+
+ # Get multicast effective routes
+ for route in conf.list_effective_nodes('route'):
+ mroute['old_mroute'][route] = {}
+ for next_hop in conf.list_effective_nodes('route {0} next-hop'.format(route)):
+ mroute['old_mroute'][route].update({
+ next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast effective interface-routes
+ for route in conf.list_effective_nodes('interface-route'):
+ if not route in mroute['old_mroute']:
+ mroute['old_mroute'][route] = {}
+ for next_hop in conf.list_effective_nodes('interface-route {0} next-hop-interface'.format(route)):
+ mroute['old_mroute'][route].update({
+ next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast routes
+ for route in conf.list_nodes('route'):
+ mroute['mroute'][route] = {}
+ for next_hop in conf.list_nodes('route {0} next-hop'.format(route)):
+ mroute['mroute'][route].update({
+ next_hop : conf.return_value('route {0} next-hop {1} distance'.format(route, next_hop))
+ })
+
+ # Get multicast interface-routes
+ for route in conf.list_nodes('interface-route'):
+ if not route in mroute['mroute']:
+ mroute['mroute'][route] = {}
+ for next_hop in conf.list_nodes('interface-route {0} next-hop-interface'.format(route)):
+ mroute['mroute'][route].update({
+ next_hop : conf.return_value('interface-route {0} next-hop-interface {1} distance'.format(route, next_hop))
+ })
+
+ return mroute
+
+def verify(mroute):
+ if mroute is None:
+ return None
+
+ for route in mroute['mroute']:
+ route = route.split('/')
+ if IPv4Address(route[0]) < IPv4Address('224.0.0.0'):
+ raise ConfigError(route + " not a multicast network")
+
+def generate(mroute):
+ if mroute is None:
+ return None
+
+ render(config_file, 'frr/static_mcast.frr.tmpl', mroute)
+ return None
+
+def apply(mroute):
+ if mroute is None:
+ return None
+
+ if os.path.exists(config_file):
+ call(f'vtysh -d staticd -f {config_file}')
+ os.remove(config_file)
+
+ 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/salt-minion.py b/src/conf_mode/salt-minion.py
new file mode 100755
index 000000000..3343d1247
--- /dev/null
+++ b/src/conf_mode/salt-minion.py
@@ -0,0 +1,123 @@
+#!/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 copy import deepcopy
+from socket import gethostname
+from sys import exit
+from urllib3 import PoolManager
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, chown
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/salt/minion'
+master_keyfile = r'/opt/vyatta/etc/config/salt/pki/minion/master_sign.pub'
+
+default_config_data = {
+ 'hash': 'sha256',
+ 'log_level': 'warning',
+ 'master' : 'salt',
+ 'user': 'minion',
+ 'group': 'vyattacfg',
+ 'salt_id': gethostname(),
+ 'mine_interval': '60',
+ 'verify_master_pubkey_sign': 'false',
+ 'master_key': ''
+}
+
+def get_config():
+ salt = deepcopy(default_config_data)
+ conf = Config()
+ base = ['service', 'salt-minion']
+
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ if conf.exists(['hash']):
+ salt['hash'] = conf.return_value(['hash'])
+
+ if conf.exists(['master']):
+ salt['master'] = conf.return_values(['master'])
+
+ if conf.exists(['id']):
+ salt['salt_id'] = conf.return_value(['id'])
+
+ if conf.exists(['user']):
+ salt['user'] = conf.return_value(['user'])
+
+ if conf.exists(['interval']):
+ salt['interval'] = conf.return_value(['interval'])
+
+ if conf.exists(['master-key']):
+ salt['master_key'] = conf.return_value(['master-key'])
+ salt['verify_master_pubkey_sign'] = 'true'
+
+ return salt
+
+def verify(salt):
+ return None
+
+def generate(salt):
+ if not salt:
+ return None
+
+ render(config_file, 'salt-minion/minion.tmpl', salt,
+ user=salt['user'], group=salt['group'])
+
+ if not os.path.exists(master_keyfile):
+ if salt['master_key']:
+ req = PoolManager().request('GET', salt['master_key'], preload_content=False)
+
+ with open(master_keyfile, 'wb') as f:
+ while True:
+ data = req.read(1024)
+ if not data:
+ break
+ f.write(data)
+
+ req.release_conn()
+ chown(master_keyfile, salt['user'], salt['group'])
+
+ return None
+
+def apply(salt):
+ if not salt:
+ # Salt removed from running config
+ call('systemctl stop salt-minion.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call('systemctl restart salt-minion.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
new file mode 100755
index 000000000..613ec6879
--- /dev/null
+++ b/src/conf_mode/service_console-server.py
@@ -0,0 +1,103 @@
+#!/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
+
+config_file = r'/run/conserver/conserver.cf'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'console-server']
+
+ # Retrieve CLI representation as dictionary
+ proxy = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True)
+ # The retrieved dictionary will look something like this:
+ #
+ # {'device': {'usb0b2.4p1.0': {'speed': '9600'},
+ # 'usb0b2.4p1.1': {'data_bits': '8',
+ # 'parity': 'none',
+ # 'speed': '115200',
+ # 'stop_bits': '2'}}}
+
+ # 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 + ['device'])
+ if 'device' in proxy:
+ for device in proxy['device']:
+ tmp = dict_merge(default_values, proxy['device'][device])
+ proxy['device'][device] = tmp
+
+ return proxy
+
+def verify(proxy):
+ if not proxy:
+ return None
+
+ if 'device' in proxy:
+ for device in proxy['device']:
+ if 'speed' not in proxy['device'][device]:
+ raise ConfigError(f'Serial port speed must be defined for "{device}"!')
+
+ if 'ssh' in proxy['device'][device]:
+ if 'port' not in proxy['device'][device]['ssh']:
+ raise ConfigError(f'SSH port must be defined for "{device}"!')
+
+ return None
+
+def generate(proxy):
+ if not proxy:
+ return None
+
+ render(config_file, 'conserver/conserver.conf.tmpl', proxy)
+ return None
+
+def apply(proxy):
+ call('systemctl stop dropbear@*.service conserver-server.service')
+
+ if not proxy:
+ if os.path.isfile(config_file):
+ os.unlink(config_file)
+ return None
+
+ call('systemctl restart conserver-server.service')
+
+ if 'device' in proxy:
+ for device in proxy['device']:
+ if 'ssh' in proxy['device'][device]:
+ port = proxy['device'][device]['ssh']['port']
+ call(f'systemctl restart dropbear@{device}.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_ids_fastnetmon.py b/src/conf_mode/service_ids_fastnetmon.py
new file mode 100755
index 000000000..d46f9578e
--- /dev/null
+++ b/src/conf_mode/service_ids_fastnetmon.py
@@ -0,0 +1,89 @@
+#!/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():
+ 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"]))
+
+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)
+
+ 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
new file mode 100755
index 000000000..553cc2e97
--- /dev/null
+++ b/src/conf_mode/service_ipoe-server.py
@@ -0,0 +1,309 @@
+#!/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.validate import is_ipv4
+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_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():
+ 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-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)
+
+ 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_mdns-repeater.py b/src/conf_mode/service_mdns-repeater.py
new file mode 100755
index 000000000..1a6b2c328
--- /dev/null
+++ b/src/conf_mode/service_mdns-repeater.py
@@ -0,0 +1,89 @@
+#!/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 netifaces import ifaddresses, interfaces, AF_INET
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/default/mdns-repeater'
+
+def get_config():
+ conf = Config()
+ base = ['service', 'mdns', 'repeater']
+ mdns = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ return mdns
+
+def verify(mdns):
+ if not mdns:
+ return None
+
+ if 'disable' in mdns:
+ return None
+
+ # We need at least two interfaces to repeat mDNS advertisments
+ if 'interface' not in mdns or len(mdns['interface']) < 2:
+ raise ConfigError('mDNS repeater requires at least 2 configured interfaces!')
+
+ # For mdns-repeater to work it is essential that the interfaces has
+ # an IPv4 address assigned
+ for interface in mdns['interface']:
+ if interface not in interfaces():
+ raise ConfigError(f'Interface "{interface}" does not exist!')
+
+ if AF_INET not in ifaddresses(interface):
+ raise ConfigError('mDNS repeater requires an IPv4 address to be '
+ f'configured on interface "{interface}"')
+
+ return None
+
+def generate(mdns):
+ if not mdns:
+ return None
+
+ if 'disable' in mdns:
+ print('Warning: mDNS repeater will be deactivated because it is disabled')
+ return None
+
+ render(config_file, 'mdns-repeater/mdns-repeater.tmpl', mdns)
+ return None
+
+def apply(mdns):
+ if not mdns or 'disable' in mdns:
+ call('systemctl stop mdns-repeater.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call('systemctl restart mdns-repeater.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_pppoe-server.py b/src/conf_mode/service_pppoe-server.py
new file mode 100755
index 000000000..39d34a7e2
--- /dev/null
+++ b/src/conf_mode/service_pppoe-server.py
@@ -0,0 +1,473 @@
+#!/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.validate import is_ipv4
+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'
+
+default_config_data = {
+ 'auth_mode': 'local',
+ 'auth_proto': ['auth_mschap_v2', 'auth_mschap_v1', 'auth_chap_md5', 'auth_pap'],
+ 'chap_secrets_file': pppoe_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool': '',
+ 'client_ip_subnets': [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ 'concentrator': 'vyos-ac',
+ 'interfaces': [],
+ 'local_users' : [],
+
+ 'svc_name': [],
+ 'dnsv4': [],
+ 'dnsv6': [],
+ 'wins': [],
+ 'mtu': '1492',
+
+ 'limits_burst': '',
+ 'limits_connections': '',
+ 'limits_timeout': '',
+
+ 'pado_delay': '',
+ 'ppp_ccp': False,
+ 'ppp_gw': '',
+ 'ppp_ipv4': '',
+ 'ppp_ipv6': '',
+ 'ppp_ipv6_accept_peer_intf_id': False,
+ 'ppp_ipv6_intf_id': '',
+ 'ppp_ipv6_peer_intf_id': '',
+ 'ppp_echo_failure': '3',
+ 'ppp_echo_interval': '30',
+ 'ppp_echo_timeout': '0',
+ 'ppp_min_mtu': '',
+ 'ppp_mppe': 'prefer',
+ 'ppp_mru': '',
+
+ 'radius_server': [],
+ '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': '',
+ 'sesscrtl': 'replace',
+ 'snmp': False,
+ 'thread_cnt': get_half_cpus()
+}
+
+def get_config():
+ conf = Config()
+ base_path = ['service', 'pppoe-server']
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+ pppoe = deepcopy(default_config_data)
+
+ # general options
+ if conf.exists(['access-concentrator']):
+ pppoe['concentrator'] = conf.return_value(['access-concentrator'])
+
+ if conf.exists(['service-name']):
+ pppoe['svc_name'] = conf.return_values(['service-name'])
+
+ if conf.exists(['interface']):
+ for interface in conf.list_nodes(['interface']):
+ conf.set_level(base_path + ['interface', interface])
+ tmp = {
+ 'name': interface,
+ 'vlans': []
+ }
+
+ if conf.exists(['vlan-id']):
+ tmp['vlans'] += conf.return_values(['vlan-id'])
+
+ if conf.exists(['vlan-range']):
+ tmp['vlans'] += conf.return_values(['vlan-range'])
+
+ pppoe['interfaces'].append(tmp)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['local-ip']):
+ pppoe['ppp_gw'] = conf.return_value(['local-ip'])
+
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ pppoe['dnsv4'].append(name_server)
+ else:
+ pppoe['dnsv6'].append(name_server)
+
+ if conf.exists(['wins-server']):
+ pppoe['wins'] = conf.return_values(['wins-server'])
+
+
+ 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'])
+ pppoe['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0)
+
+ if conf.exists(['client-ip-pool', 'subnet']):
+ pppoe['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet'])
+
+
+ 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'])
+
+ pppoe['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'])
+
+ pppoe['client_ipv6_delegate_prefix'].append(tmp)
+
+
+ if conf.exists(['limits']):
+ if conf.exists(['limits', 'burst']):
+ pppoe['limits_burst'] = conf.return_value(['limits', 'burst'])
+
+ if conf.exists(['limits', 'connection-limit']):
+ pppoe['limits_connections'] = conf.return_value(['limits', 'connection-limit'])
+
+ if conf.exists(['limits', 'timeout']):
+ pppoe['limits_timeout'] = conf.return_value(['limits', 'timeout'])
+
+
+ if conf.exists(['snmp']):
+ pppoe['snmp'] = True
+
+ if conf.exists(['snmp', 'master-agent']):
+ pppoe['snmp'] = 'enable-ma'
+
+ # authentication mode local
+ if conf.exists(['authentication', 'mode']):
+ pppoe['auth_mode'] = conf.return_value(['authentication', 'mode'])
+
+ 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'])
+
+ pppoe['local_users'].append(user)
+
+ conf.set_level(base_path)
+
+ if conf.exists(['authentication', 'protocols']):
+ auth_mods = {
+ 'mschap-v2': 'auth_mschap_v2',
+ 'mschap': 'auth_mschap_v1',
+ 'chap': 'auth_chap_md5',
+ 'pap': 'auth_pap'
+ }
+
+ pppoe['auth_proto'] = []
+ for proto in conf.return_values(['authentication', 'protocols']):
+ pppoe['auth_proto'].append(auth_mods[proto])
+
+ #
+ # authentication mode radius servers and settings
+ if conf.exists(['authentication', 'mode', 'radius']):
+
+ 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']):
+ pppoe['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ pppoe['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ pppoe['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ pppoe['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ pppoe['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ pppoe['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ pppoe['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'])
+
+ pppoe['radius_dynamic_author'] = dae
+
+ # RADIUS based rate-limiter
+ if conf.exists(['rate-limit', 'enable']):
+ pppoe['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ pppoe['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ pppoe['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ # re-set config level
+ conf.set_level(base_path)
+
+ if conf.exists(['mtu']):
+ pppoe['mtu'] = conf.return_value(['mtu'])
+
+ if conf.exists(['session-control']):
+ pppoe['sesscrtl'] = conf.return_value(['session-control'])
+
+ # ppp_options
+ if conf.exists(['ppp-options']):
+ conf.set_level(base_path + ['ppp-options'])
+
+ if conf.exists(['ccp']):
+ pppoe['ppp_ccp'] = True
+
+ if conf.exists(['ipv4']):
+ pppoe['ppp_ipv4'] = conf.return_value(['ipv4'])
+
+ if conf.exists(['ipv6']):
+ pppoe['ppp_ipv6'] = conf.return_value(['ipv6'])
+
+ if conf.exists(['ipv6-accept-peer-intf-id']):
+ pppoe['ppp_ipv6_peer_intf_id'] = True
+
+ if conf.exists(['ipv6-intf-id']):
+ pppoe['ppp_ipv6_intf_id'] = conf.return_value(['ipv6-intf-id'])
+
+ if conf.exists(['ipv6-peer-intf-id']):
+ pppoe['ppp_ipv6_peer_intf_id'] = conf.return_value(['ipv6-peer-intf-id'])
+
+ if conf.exists(['lcp-echo-failure']):
+ pppoe['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-failure']):
+ pppoe['ppp_echo_interval'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-timeout']):
+ pppoe['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout'])
+
+ if conf.exists(['min-mtu']):
+ pppoe['ppp_min_mtu'] = conf.return_value(['min-mtu'])
+
+ if conf.exists(['mppe']):
+ pppoe['ppp_mppe'] = conf.return_value(['mppe'])
+
+ if conf.exists(['mru']):
+ pppoe['ppp_mru'] = conf.return_value(['mru'])
+
+ if conf.exists(['pado-delay']):
+ pppoe['pado_delay'] = '0'
+ a = {}
+ for id in conf.list_nodes(['pado-delay']):
+ if not conf.return_value(['pado-delay', id, 'sessions']):
+ a[id] = 0
+ else:
+ a[id] = conf.return_value(['pado-delay', id, 'sessions'])
+
+ for k in sorted(a.keys()):
+ if k != sorted(a.keys())[-1]:
+ pppoe['pado_delay'] += ",{0}:{1}".format(k, a[k])
+ else:
+ pppoe['pado_delay'] += ",{0}:{1}".format('-1', a[k])
+
+ return pppoe
+
+
+def verify(pppoe):
+ if not pppoe:
+ return None
+
+ # vertify auth settings
+ if pppoe['auth_mode'] == 'local':
+ if not pppoe['local_users']:
+ raise ConfigError('PPPoE local auth mode requires local users to be configured!')
+
+ for user in pppoe['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ # if up/download is set, check that both have a value
+ if user['upload'] and not user['download']:
+ raise ConfigError(f'Download speed value required for local user "{username}"')
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f'Upload speed value required for local user "{username}"')
+
+ elif pppoe['auth_mode'] == 'radius':
+ if len(pppoe['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in pppoe['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+ if len(pppoe['wins']) > 2:
+ raise ConfigError('Not more then two IPv4 WINS name-servers can be configured')
+
+ if len(pppoe['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ if len(pppoe['dnsv6']) > 3:
+ raise ConfigError('Not more then three IPv6 DNS name-servers can be configured')
+
+ if not pppoe['interfaces']:
+ raise ConfigError('At least one listen interface must be defined!')
+
+ # local ippool and gateway settings config checks
+ if pppoe['client_ip_subnets'] or pppoe['client_ip_pool']:
+ if not pppoe['ppp_gw']:
+ raise ConfigError('PPPoE server requires local IP to be configured')
+
+ if pppoe['ppp_gw'] and not pppoe['client_ip_subnets'] and not pppoe['client_ip_pool']:
+ print("Warning: No PPPoE client pool defined")
+
+ return None
+
+
+def generate(pppoe):
+ if not pppoe:
+ return None
+
+ render(pppoe_conf, 'accel-ppp/pppoe.config.tmpl', pppoe, trim_blocks=True)
+
+ if pppoe['local_users']:
+ render(pppoe_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pppoe, trim_blocks=True)
+ os.chmod(pppoe_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ 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
new file mode 100755
index 000000000..4e1c432ab
--- /dev/null
+++ b/src/conf_mode/service_router-advert.py
@@ -0,0 +1,117 @@
+#!/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():
+ 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)
+ 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/snmp.py b/src/conf_mode/snmp.py
new file mode 100755
index 000000000..e9806ef47
--- /dev/null
+++ b/src/conf_mode/snmp.py
@@ -0,0 +1,581 @@
+#!/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.configverify import verify_vrf
+from vyos.snmpv3_hashgen import plaintext_to_md5, plaintext_to_sha1, random
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_ipv4, is_addr_assigned
+from vyos.version import get_version_data
+from vyos import ConfigError, airbag
+airbag.enable()
+
+config_file_client = r'/etc/snmp/snmp.conf'
+config_file_daemon = r'/etc/snmp/snmpd.conf'
+config_file_access = r'/usr/share/snmp/snmpd.conf'
+config_file_user = r'/var/lib/snmp/snmpd.conf'
+default_script_dir = r'/config/user-data/'
+systemd_override = r'/etc/systemd/system/snmpd.service.d/override.conf'
+
+# SNMP OIDs used to mark auth/priv type
+OIDs = {
+ 'md5' : '.1.3.6.1.6.3.10.1.1.2',
+ 'sha' : '.1.3.6.1.6.3.10.1.1.3',
+ 'aes' : '.1.3.6.1.6.3.10.1.2.4',
+ 'des' : '.1.3.6.1.6.3.10.1.2.2',
+ 'none': '.1.3.6.1.6.3.10.1.2.1'
+}
+
+default_config_data = {
+ 'listen_on': [],
+ 'listen_address': [],
+ 'ipv6_enabled': 'True',
+ 'communities': [],
+ 'smux_peers': [],
+ 'location' : '',
+ 'description' : '',
+ 'contact' : '',
+ 'trap_source': '',
+ 'trap_targets': [],
+ 'vyos_user': '',
+ 'vyos_user_pass': '',
+ 'version': '',
+ 'v3_enabled': 'False',
+ 'v3_engineid': '',
+ 'v3_groups': [],
+ 'v3_traps': [],
+ 'v3_users': [],
+ 'v3_views': [],
+ 'script_ext': []
+}
+
+def rmfile(file):
+ if os.path.isfile(file):
+ os.unlink(file)
+
+def get_config():
+ snmp = default_config_data
+ conf = Config()
+ if not conf.exists('service snmp'):
+ return None
+ else:
+ if conf.exists('system ipv6 disable'):
+ snmp['ipv6_enabled'] = False
+
+ conf.set_level('service snmp')
+
+ version_data = get_version_data()
+ snmp['version'] = version_data['version']
+
+ # create an internal snmpv3 user of the form 'vyosxxxxxxxxxxxxxxxx'
+ snmp['vyos_user'] = 'vyos' + random(8)
+ snmp['vyos_user_pass'] = random(16)
+
+ if conf.exists('community'):
+ for name in conf.list_nodes('community'):
+ community = {
+ 'name': name,
+ 'authorization': 'ro',
+ 'network_v4': [],
+ 'network_v6': [],
+ 'has_source' : False
+ }
+
+ if conf.exists('community {0} authorization'.format(name)):
+ community['authorization'] = conf.return_value('community {0} authorization'.format(name))
+
+ # Subnet of SNMP client(s) allowed to contact system
+ if conf.exists('community {0} network'.format(name)):
+ for addr in conf.return_values('community {0} network'.format(name)):
+ if is_ipv4(addr):
+ community['network_v4'].append(addr)
+ else:
+ community['network_v6'].append(addr)
+
+ # IP address of SNMP client allowed to contact system
+ if conf.exists('community {0} client'.format(name)):
+ for addr in conf.return_values('community {0} client'.format(name)):
+ if is_ipv4(addr):
+ community['network_v4'].append(addr)
+ else:
+ community['network_v6'].append(addr)
+
+ if (len(community['network_v4']) > 0) or (len(community['network_v6']) > 0):
+ community['has_source'] = True
+
+ snmp['communities'].append(community)
+
+ if conf.exists('contact'):
+ snmp['contact'] = conf.return_value('contact')
+
+ if conf.exists('description'):
+ snmp['description'] = conf.return_value('description')
+
+ if conf.exists('listen-address'):
+ for addr in conf.list_nodes('listen-address'):
+ port = '161'
+ if conf.exists('listen-address {0} port'.format(addr)):
+ port = conf.return_value('listen-address {0} port'.format(addr))
+
+ snmp['listen_address'].append((addr, port))
+
+ # Always listen on localhost if an explicit address has been configured
+ # This is a safety measure to not end up with invalid listen addresses
+ # that are not configured on this system. See https://phabricator.vyos.net/T850
+ if not '127.0.0.1' in conf.list_nodes('listen-address'):
+ snmp['listen_address'].append(('127.0.0.1', '161'))
+
+ if not '::1' in conf.list_nodes('listen-address'):
+ snmp['listen_address'].append(('::1', '161'))
+
+ if conf.exists('location'):
+ snmp['location'] = conf.return_value('location')
+
+ if conf.exists('smux-peer'):
+ snmp['smux_peers'] = conf.return_values('smux-peer')
+
+ if conf.exists('trap-source'):
+ snmp['trap_source'] = conf.return_value('trap-source')
+
+ if conf.exists('trap-target'):
+ for target in conf.list_nodes('trap-target'):
+ trap_tgt = {
+ 'target': target,
+ 'community': '',
+ 'port': ''
+ }
+
+ if conf.exists('trap-target {0} community'.format(target)):
+ trap_tgt['community'] = conf.return_value('trap-target {0} community'.format(target))
+
+ if conf.exists('trap-target {0} port'.format(target)):
+ trap_tgt['port'] = conf.return_value('trap-target {0} port'.format(target))
+
+ snmp['trap_targets'].append(trap_tgt)
+
+ if conf.exists('script-extensions'):
+ for extname in conf.list_nodes('script-extensions extension-name'):
+ conf_script = conf.return_value('script-extensions extension-name {} script'.format(extname))
+ # if script has not absolute path, use pre configured path
+ if "/" not in conf_script:
+ conf_script = default_script_dir + conf_script
+
+ extension = {
+ 'name': extname,
+ 'script' : conf_script
+ }
+
+ snmp['script_ext'].append(extension)
+
+ if conf.exists('vrf'):
+ # Append key to dict but don't place it in the default dictionary.
+ # This is required to make the override.conf.tmpl work until we
+ # migrate to get_config_dict().
+ snmp['vrf'] = conf.return_value('vrf')
+
+
+ #########################################################################
+ # ____ _ _ __ __ ____ _____ #
+ # / ___|| \ | | \/ | _ \ __ _|___ / #
+ # \___ \| \| | |\/| | |_) | \ \ / / |_ \ #
+ # ___) | |\ | | | | __/ \ V / ___) | #
+ # |____/|_| \_|_| |_|_| \_/ |____/ #
+ # #
+ # now take care about the fancy SNMP v3 stuff, or bail out eraly #
+ #########################################################################
+ if not conf.exists('v3'):
+ return snmp
+ else:
+ snmp['v3_enabled'] = True
+
+ # 'set service snmp v3 engineid'
+ if conf.exists('v3 engineid'):
+ snmp['v3_engineid'] = conf.return_value('v3 engineid')
+
+ # 'set service snmp v3 group'
+ if conf.exists('v3 group'):
+ for group in conf.list_nodes('v3 group'):
+ v3_group = {
+ 'name': group,
+ 'mode': 'ro',
+ 'seclevel': 'auth',
+ 'view': ''
+ }
+
+ if conf.exists('v3 group {0} mode'.format(group)):
+ v3_group['mode'] = conf.return_value('v3 group {0} mode'.format(group))
+
+ if conf.exists('v3 group {0} seclevel'.format(group)):
+ v3_group['seclevel'] = conf.return_value('v3 group {0} seclevel'.format(group))
+
+ if conf.exists('v3 group {0} view'.format(group)):
+ v3_group['view'] = conf.return_value('v3 group {0} view'.format(group))
+
+ snmp['v3_groups'].append(v3_group)
+
+ # 'set service snmp v3 trap-target'
+ if conf.exists('v3 trap-target'):
+ for trap in conf.list_nodes('v3 trap-target'):
+ trap_cfg = {
+ 'ipAddr': trap,
+ 'secName': '',
+ 'authProtocol': 'md5',
+ 'authPassword': '',
+ 'authMasterKey': '',
+ 'privProtocol': 'des',
+ 'privPassword': '',
+ 'privMasterKey': '',
+ 'ipProto': 'udp',
+ 'ipPort': '162',
+ 'type': '',
+ 'secLevel': 'noAuthNoPriv'
+ }
+
+ if conf.exists('v3 trap-target {0} user'.format(trap)):
+ # Set the securityName used for authenticated SNMPv3 messages.
+ trap_cfg['secName'] = conf.return_value('v3 trap-target {0} user'.format(trap))
+
+ if conf.exists('v3 trap-target {0} auth type'.format(trap)):
+ # Set the authentication protocol (MD5 or SHA) used for authenticated SNMPv3 messages
+ # cmdline option '-a'
+ trap_cfg['authProtocol'] = conf.return_value('v3 trap-target {0} auth type'.format(trap))
+
+ if conf.exists('v3 trap-target {0} auth plaintext-password'.format(trap)):
+ # Set the authentication pass phrase used for authenticated SNMPv3 messages.
+ # cmdline option '-A'
+ trap_cfg['authPassword'] = conf.return_value('v3 trap-target {0} auth plaintext-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} auth encrypted-password'.format(trap)):
+ # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master authentication keys.
+ # cmdline option '-3m'
+ trap_cfg['authMasterKey'] = conf.return_value('v3 trap-target {0} auth encrypted-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} privacy type'.format(trap)):
+ # Set the privacy protocol (DES or AES) used for encrypted SNMPv3 messages.
+ # cmdline option '-x'
+ trap_cfg['privProtocol'] = conf.return_value('v3 trap-target {0} privacy type'.format(trap))
+
+ if conf.exists('v3 trap-target {0} privacy plaintext-password'.format(trap)):
+ # Set the privacy pass phrase used for encrypted SNMPv3 messages.
+ # cmdline option '-X'
+ trap_cfg['privPassword'] = conf.return_value('v3 trap-target {0} privacy plaintext-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} privacy encrypted-password'.format(trap)):
+ # Sets the keys to be used for SNMPv3 transactions. These options allow you to set the master encryption keys.
+ # cmdline option '-3M'
+ trap_cfg['privMasterKey'] = conf.return_value('v3 trap-target {0} privacy encrypted-password'.format(trap))
+
+ if conf.exists('v3 trap-target {0} protocol'.format(trap)):
+ trap_cfg['ipProto'] = conf.return_value('v3 trap-target {0} protocol'.format(trap))
+
+ if conf.exists('v3 trap-target {0} port'.format(trap)):
+ trap_cfg['ipPort'] = conf.return_value('v3 trap-target {0} port'.format(trap))
+
+ if conf.exists('v3 trap-target {0} type'.format(trap)):
+ trap_cfg['type'] = conf.return_value('v3 trap-target {0} type'.format(trap))
+
+ # Determine securityLevel used for SNMPv3 messages (noAuthNoPriv|authNoPriv|authPriv).
+ # Appropriate pass phrase(s) must provided when using any level higher than noAuthNoPriv.
+ if trap_cfg['authPassword'] or trap_cfg['authMasterKey']:
+ if trap_cfg['privProtocol'] or trap_cfg['privPassword']:
+ trap_cfg['secLevel'] = 'authPriv'
+ else:
+ trap_cfg['secLevel'] = 'authNoPriv'
+
+ snmp['v3_traps'].append(trap_cfg)
+
+ # 'set service snmp v3 user'
+ if conf.exists('v3 user'):
+ for user in conf.list_nodes('v3 user'):
+ user_cfg = {
+ 'name': user,
+ 'authMasterKey': '',
+ 'authPassword': '',
+ 'authProtocol': 'md5',
+ 'authOID': 'none',
+ 'group': '',
+ 'mode': 'ro',
+ 'privMasterKey': '',
+ 'privPassword': '',
+ 'privOID': '',
+ 'privProtocol': 'des'
+ }
+
+ # v3 user {0} auth
+ if conf.exists('v3 user {0} auth encrypted-password'.format(user)):
+ user_cfg['authMasterKey'] = conf.return_value('v3 user {0} auth encrypted-password'.format(user))
+
+ if conf.exists('v3 user {0} auth plaintext-password'.format(user)):
+ user_cfg['authPassword'] = conf.return_value('v3 user {0} auth plaintext-password'.format(user))
+
+ # load default value
+ type = user_cfg['authProtocol']
+ if conf.exists('v3 user {0} auth type'.format(user)):
+ type = conf.return_value('v3 user {0} auth type'.format(user))
+
+ # (re-)update with either default value or value from CLI
+ user_cfg['authProtocol'] = type
+ user_cfg['authOID'] = OIDs[type]
+
+ # v3 user {0} group
+ if conf.exists('v3 user {0} group'.format(user)):
+ user_cfg['group'] = conf.return_value('v3 user {0} group'.format(user))
+
+ # v3 user {0} mode
+ if conf.exists('v3 user {0} mode'.format(user)):
+ user_cfg['mode'] = conf.return_value('v3 user {0} mode'.format(user))
+
+ # v3 user {0} privacy
+ if conf.exists('v3 user {0} privacy encrypted-password'.format(user)):
+ user_cfg['privMasterKey'] = conf.return_value('v3 user {0} privacy encrypted-password'.format(user))
+
+ if conf.exists('v3 user {0} privacy plaintext-password'.format(user)):
+ user_cfg['privPassword'] = conf.return_value('v3 user {0} privacy plaintext-password'.format(user))
+
+ # load default value
+ type = user_cfg['privProtocol']
+ if conf.exists('v3 user {0} privacy type'.format(user)):
+ type = conf.return_value('v3 user {0} privacy type'.format(user))
+
+ # (re-)update with either default value or value from CLI
+ user_cfg['privProtocol'] = type
+ user_cfg['privOID'] = OIDs[type]
+
+ snmp['v3_users'].append(user_cfg)
+
+ # 'set service snmp v3 view'
+ if conf.exists('v3 view'):
+ for view in conf.list_nodes('v3 view'):
+ view_cfg = {
+ 'name': view,
+ 'oids': []
+ }
+
+ if conf.exists('v3 view {0} oid'.format(view)):
+ for oid in conf.list_nodes('v3 view {0} oid'.format(view)):
+ oid_cfg = {
+ 'oid': oid
+ }
+ view_cfg['oids'].append(oid_cfg)
+ snmp['v3_views'].append(view_cfg)
+
+ return snmp
+
+def verify(snmp):
+ if snmp is None:
+ # we can not delete SNMP when LLDP is configured with SNMP
+ conf = Config()
+ if conf.exists('service lldp snmp enable'):
+ raise ConfigError('Can not delete SNMP service, as LLDP still uses SNMP!')
+
+ return None
+
+ ### check if the configured script actually exist
+ if snmp['script_ext']:
+ for ext in snmp['script_ext']:
+ if not os.path.isfile(ext['script']):
+ print ("WARNING: script: {} doesn't exist".format(ext['script']))
+ else:
+ chmod_755(ext['script'])
+
+ for listen in snmp['listen_address']:
+ addr = listen[0]
+ port = listen[1]
+
+ if is_ipv4(addr):
+ # example: udp:127.0.0.1:161
+ listen = 'udp:' + addr + ':' + port
+ elif snmp['ipv6_enabled']:
+ # example: udp6:[::1]:161
+ listen = 'udp6:' + '[' + addr + ']' + ':' + port
+
+ # We only wan't to configure addresses that exist on the system.
+ # Hint the user if they don't exist
+ if is_addr_assigned(addr):
+ snmp['listen_on'].append(listen)
+ else:
+ print('WARNING: SNMP listen address {0} not configured!'.format(addr))
+
+ verify_vrf(snmp)
+
+ # bail out early if SNMP v3 is not configured
+ if not snmp['v3_enabled']:
+ return None
+
+ if 'v3_groups' in snmp.keys():
+ for group in snmp['v3_groups']:
+ #
+ # A view must exist prior to mapping it into a group
+ #
+ if 'view' in group.keys():
+ error = True
+ if 'v3_views' in snmp.keys():
+ for view in snmp['v3_views']:
+ if view['name'] == group['view']:
+ error = False
+ if error:
+ raise ConfigError('You must create view "{0}" first'.format(group['view']))
+ else:
+ raise ConfigError('"view" must be specified')
+
+ if not 'mode' in group.keys():
+ raise ConfigError('"mode" must be specified')
+
+ if not 'seclevel' in group.keys():
+ raise ConfigError('"seclevel" must be specified')
+
+ if 'v3_traps' in snmp.keys():
+ for trap in snmp['v3_traps']:
+ if trap['authPassword'] and trap['authMasterKey']:
+ raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap auth')
+
+ if trap['authPassword'] == '' and trap['authMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-password or plaintext-key for trap auth')
+
+ if trap['privPassword'] and trap['privMasterKey']:
+ raise ConfigError('Must specify only one of encrypted-password/plaintext-key for trap privacy')
+
+ if trap['privPassword'] == '' and trap['privMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-password or plaintext-key for trap privacy')
+
+ if not 'type' in trap.keys():
+ raise ConfigError('v3 trap: "type" must be specified')
+
+ if not 'authPassword' and 'authMasterKey' in trap.keys():
+ raise ConfigError('v3 trap: "auth" must be specified')
+
+ if not 'authProtocol' in trap.keys():
+ raise ConfigError('v3 trap: "protocol" must be specified')
+
+ if not 'privPassword' and 'privMasterKey' in trap.keys():
+ raise ConfigError('v3 trap: "user" must be specified')
+
+ if 'v3_users' in snmp.keys():
+ for user in snmp['v3_users']:
+ #
+ # Group must exist prior to mapping it into a group
+ # seclevel will be extracted from group
+ #
+ if user['group']:
+ error = True
+ if 'v3_groups' in snmp.keys():
+ for group in snmp['v3_groups']:
+ if group['name'] == user['group']:
+ seclevel = group['seclevel']
+ error = False
+
+ if error:
+ raise ConfigError('You must create group "{0}" first'.format(user['group']))
+
+ # Depending on the configured security level the user has to provide additional info
+ if (not user['authPassword'] and not user['authMasterKey']):
+ raise ConfigError('Must specify encrypted-password or plaintext-key for user auth')
+
+ if user['privPassword'] == '' and user['privMasterKey'] == '':
+ raise ConfigError('Must specify encrypted-password or plaintext-key for user privacy')
+
+ if user['mode'] == '':
+ raise ConfigError('Must specify user mode ro/rw')
+
+ if 'v3_views' in snmp.keys():
+ for view in snmp['v3_views']:
+ if not view['oids']:
+ raise ConfigError('Must configure an oid')
+
+ return None
+
+def generate(snmp):
+ #
+ # As we are manipulating the snmpd user database we have to stop it first!
+ # This is even save if service is going to be removed
+ call('systemctl stop snmpd.service')
+ config_files = [config_file_client, config_file_daemon, config_file_access,
+ config_file_user, systemd_override]
+ for file in config_files:
+ rmfile(file)
+
+ if not snmp:
+ return None
+
+ if 'v3_users' in snmp.keys():
+ # net-snmp is now regenerating the configuration file in the background
+ # thus we need to re-open and re-read the file as the content changed.
+ # After that we can no read the encrypted password from the config and
+ # replace the CLI plaintext password with its encrypted version.
+ os.environ["vyos_libexec_dir"] = "/usr/libexec/vyos"
+
+ for user in snmp['v3_users']:
+ if user['authProtocol'] == 'sha':
+ hash = plaintext_to_sha1
+ else:
+ hash = plaintext_to_md5
+
+ if user['authPassword']:
+ user['authMasterKey'] = hash(user['authPassword'], snmp['v3_engineid'])
+ user['authPassword'] = ''
+
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" auth encrypted-password "{authMasterKey}" > /dev/null'.format(**user))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" auth plaintext-password > /dev/null'.format(**user))
+
+ if user['privPassword']:
+ user['privMasterKey'] = hash(user['privPassword'], snmp['v3_engineid'])
+ user['privPassword'] = ''
+
+ call('/opt/vyatta/sbin/my_set service snmp v3 user "{name}" privacy encrypted-password "{privMasterKey}" > /dev/null'.format(**user))
+ call('/opt/vyatta/sbin/my_delete service snmp v3 user "{name}" privacy plaintext-password > /dev/null'.format(**user))
+
+ # Write client config file
+ render(config_file_client, 'snmp/etc.snmp.conf.tmpl', snmp)
+ # Write server config file
+ render(config_file_daemon, 'snmp/etc.snmpd.conf.tmpl', snmp)
+ # Write access rights config file
+ render(config_file_access, 'snmp/usr.snmpd.conf.tmpl', snmp)
+ # Write access rights config file
+ render(config_file_user, 'snmp/var.snmpd.conf.tmpl', snmp)
+ # Write daemon configuration file
+ render(systemd_override, 'snmp/override.conf.tmpl', snmp)
+
+ return None
+
+def apply(snmp):
+ # Always reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if not snmp:
+ return None
+
+ # start SNMP daemon
+ call('systemctl restart snmpd.service')
+
+ # Enable AgentX in FRR
+ call('vtysh -c "configure terminal" -c "agentx" >/dev/null')
+
+ 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
new file mode 100755
index 000000000..7b262565a
--- /dev/null
+++ b/src/conf_mode/ssh.py
@@ -0,0 +1,94 @@
+#!/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 netifaces import interfaces
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+from vyos.xml import defaults
+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():
+ 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
+
+ if 'vrf' in ssh.keys() and ssh['vrf'] not in interfaces():
+ raise ConfigError('VRF "{vrf}" does not exist'.format(**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)
+
+ 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-ip.py b/src/conf_mode/system-ip.py
new file mode 100755
index 000000000..85f1e3771
--- /dev/null
+++ b/src/conf_mode/system-ip.py
@@ -0,0 +1,85 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'arp_table': 8192,
+ 'ipv4_forward': '1',
+ 'mp_unreach_nexthop': '0',
+ 'mp_layer4_hashing': '0'
+}
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ ip_opt = deepcopy(default_config_data)
+ conf = Config()
+ conf.set_level('system ip')
+ if conf.exists(''):
+ if conf.exists('arp table-size'):
+ ip_opt['arp_table'] = int(conf.return_value('arp table-size'))
+
+ if conf.exists('disable-forwarding'):
+ ip_opt['ipv4_forward'] = '0'
+
+ if conf.exists('multipath ignore-unreachable-nexthops'):
+ ip_opt['mp_unreach_nexthop'] = '1'
+
+ if conf.exists('multipath layer4-hashing'):
+ ip_opt['mp_layer4_hashing'] = '1'
+
+ return ip_opt
+
+def verify(ip_opt):
+ pass
+
+def generate(ip_opt):
+ pass
+
+def apply(ip_opt):
+ # apply ARP threshold values
+ sysctl('net.ipv4.neigh.default.gc_thresh3', ip_opt['arp_table'])
+ sysctl('net.ipv4.neigh.default.gc_thresh2', ip_opt['arp_table'] // 2)
+ sysctl('net.ipv4.neigh.default.gc_thresh1', ip_opt['arp_table'] // 8)
+
+ # enable/disable IPv4 forwarding
+ with open('/proc/sys/net/ipv4/conf/all/forwarding', 'w') as f:
+ f.write(ip_opt['ipv4_forward'])
+
+ # configure multipath
+ sysctl('net.ipv4.fib_multipath_use_neigh', ip_opt['mp_unreach_nexthop'])
+ sysctl('net.ipv4.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
+
+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-ipv6.py b/src/conf_mode/system-ipv6.py
new file mode 100755
index 000000000..3417c609d
--- /dev/null
+++ b/src/conf_mode/system-ipv6.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2019 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import sys
+
+from sys import exit
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+ipv6_disable_file = '/etc/modprobe.d/vyos_disable_ipv6.conf'
+
+default_config_data = {
+ 'reboot_message': False,
+ 'ipv6_forward': '1',
+ 'disable_addr_assignment': False,
+ 'mp_layer4_hashing': '0',
+ 'neighbor_cache': 8192,
+ 'strict_dad': '1'
+
+}
+
+def sysctl(name, value):
+ call('sysctl -wq {}={}'.format(name, value))
+
+def get_config():
+ ip_opt = deepcopy(default_config_data)
+ conf = Config()
+ conf.set_level('system ipv6')
+ if conf.exists(''):
+ ip_opt['disable_addr_assignment'] = conf.exists('disable')
+ if conf.exists_effective('disable') != conf.exists('disable'):
+ ip_opt['reboot_message'] = True
+
+ if conf.exists('disable-forwarding'):
+ ip_opt['ipv6_forward'] = '0'
+
+ if conf.exists('multipath layer4-hashing'):
+ ip_opt['mp_layer4_hashing'] = '1'
+
+ if conf.exists('neighbor table-size'):
+ ip_opt['neighbor_cache'] = int(conf.return_value('neighbor table-size'))
+
+ if conf.exists('strict-dad'):
+ ip_opt['strict_dad'] = 2
+
+ return ip_opt
+
+def verify(ip_opt):
+ pass
+
+def generate(ip_opt):
+ pass
+
+def apply(ip_opt):
+ # disable IPv6 address assignment
+ if ip_opt['disable_addr_assignment']:
+ with open(ipv6_disable_file, 'w') as f:
+ f.write('options ipv6 disable_ipv6=1')
+ else:
+ if os.path.exists(ipv6_disable_file):
+ os.unlink(ipv6_disable_file)
+
+ if ip_opt['reboot_message']:
+ print('Changing IPv6 disable parameter will only take affect\n' \
+ 'when the system is rebooted.')
+
+ # configure multipath
+ sysctl('net.ipv6.fib_multipath_hash_policy', ip_opt['mp_layer4_hashing'])
+
+ # apply neighbor table threshold values
+ sysctl('net.ipv6.neigh.default.gc_thresh3', ip_opt['neighbor_cache'])
+ sysctl('net.ipv6.neigh.default.gc_thresh2', ip_opt['neighbor_cache'] // 2)
+ sysctl('net.ipv6.neigh.default.gc_thresh1', ip_opt['neighbor_cache'] // 8)
+
+ # enable/disable IPv6 forwarding
+ with open('/proc/sys/net/ipv6/conf/all/forwarding', 'w') as f:
+ f.write(ip_opt['ipv6_forward'])
+
+ # configure IPv6 strict-dad
+ for root, dirs, files in os.walk('/proc/sys/net/ipv6/conf'):
+ for name in files:
+ if name == "accept_dad":
+ with open(os.path.join(root, name), 'w') as f:
+ f.write(str(ip_opt['strict_dad']))
+
+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-banner.py b/src/conf_mode/system-login-banner.py
new file mode 100755
index 000000000..5c0adc921
--- /dev/null
+++ b/src/conf_mode/system-login-banner.py
@@ -0,0 +1,110 @@
+#!/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 import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+motd="""
+The programs included with the Debian GNU/Linux system are free software;
+the exact distribution terms for each program are described in the
+individual files in /usr/share/doc/*/copyright.
+
+Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
+permitted by applicable law.
+
+"""
+
+PRELOGIN_FILE = r'/etc/issue'
+PRELOGIN_NET_FILE = r'/etc/issue.net'
+POSTLOGIN_FILE = r'/etc/motd'
+
+default_config_data = {
+ 'issue': 'Welcome to VyOS - \n \l\n',
+ 'issue_net': 'Welcome to VyOS\n',
+ 'motd': motd
+}
+
+def get_config():
+ banner = default_config_data
+ conf = Config()
+ base_level = ['system', 'login', 'banner']
+
+ if not conf.exists(base_level):
+ return banner
+ else:
+ conf.set_level(base_level)
+
+ # Post-Login banner
+ if conf.exists(['post-login']):
+ tmp = conf.return_value(['post-login'])
+ # post-login banner can be empty as well
+ if tmp:
+ tmp = tmp.replace('\\n','\n')
+ tmp = tmp.replace('\\t','\t')
+ # always add newline character
+ tmp += '\n'
+ else:
+ tmp = ''
+
+ banner['motd'] = tmp
+
+ # Pre-Login banner
+ if conf.exists(['pre-login']):
+ tmp = conf.return_value(['pre-login'])
+ # pre-login banner can be empty as well
+ if tmp:
+ tmp = tmp.replace('\\n','\n')
+ tmp = tmp.replace('\\t','\t')
+ # always add newline character
+ tmp += '\n'
+ else:
+ tmp = ''
+
+ banner['issue'] = banner['issue_net'] = tmp
+
+ return banner
+
+def verify(banner):
+ pass
+
+def generate(banner):
+ pass
+
+def apply(banner):
+ with open(PRELOGIN_FILE, 'w') as f:
+ f.write(banner['issue'])
+
+ with open(PRELOGIN_NET_FILE, 'w') as f:
+ f.write(banner['issue_net'])
+
+ with open(POSTLOGIN_FILE, 'w') as f:
+ f.write(banner['motd'])
+
+ 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
new file mode 100755
index 000000000..b1dd583b5
--- /dev/null
+++ b/src/conf_mode/system-login.py
@@ -0,0 +1,403 @@
+#!/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():
+ login = default_config_data
+ 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_set system login user '{name}' "
+ "authentication plaintext-password '{password_plaintext}'"
+ .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)
+
+ 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-options.py b/src/conf_mode/system-options.py
new file mode 100755
index 000000000..0aacd19d8
--- /dev/null
+++ b/src/conf_mode/system-options.py
@@ -0,0 +1,109 @@
+#!/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 vyos.config import Config
+from vyos.template import render
+from vyos.util import call
+from vyos.validate import is_addr_assigned
+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():
+ conf = Config()
+ base = ['system', 'options']
+ options = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ 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)
+ return None
+
+def apply(options):
+ # Beep action
+ if 'beep_if_fully_booted' in options.keys():
+ call('systemctl enable vyos-beep.service')
+ else:
+ call('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_action' in options:
+ if options['ctrl_alt_del_action'] == 'reboot':
+ os.symlink('/lib/systemd/system/reboot.target', systemd_action_file)
+ elif options['ctrl_alt_del_action'] == 'poweroff':
+ os.symlink('/lib/systemd/system/poweroff.target', systemd_action_file)
+
+ if 'http_client' not in options:
+ if os.path.exists(curlrc_config):
+ os.unlink(curlrc_config)
+
+ 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.keys():
+ f.write('60')
+ else:
+ f.write('0')
+
+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-proxy.py b/src/conf_mode/system-proxy.py
new file mode 100755
index 000000000..02536c2ab
--- /dev/null
+++ b/src/conf_mode/system-proxy.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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
+import os
+import re
+
+from vyos import ConfigError
+from vyos.config import Config
+
+from vyos import airbag
+airbag.enable()
+
+proxy_def = r'/etc/profile.d/vyos-system-proxy.sh'
+
+
+def get_config():
+ c = Config()
+ if not c.exists('system proxy'):
+ return None
+
+ c.set_level('system proxy')
+
+ cnf = {
+ 'url': None,
+ 'port': None,
+ 'usr': None,
+ 'passwd': None
+ }
+
+ if c.exists('url'):
+ cnf['url'] = c.return_value('url')
+ if c.exists('port'):
+ cnf['port'] = c.return_value('port')
+ if c.exists('username'):
+ cnf['usr'] = c.return_value('username')
+ if c.exists('password'):
+ cnf['passwd'] = c.return_value('password')
+
+ return cnf
+
+
+def verify(c):
+ if not c:
+ return None
+ if not c['url'] or not c['port']:
+ raise ConfigError("proxy url and port requires a value")
+ elif c['usr'] and not c['passwd']:
+ raise ConfigError("proxy password requires a value")
+ elif not c['usr'] and c['passwd']:
+ raise ConfigError("proxy username requires a value")
+
+
+def generate(c):
+ if not c:
+ return None
+ if not c['usr']:
+ return str("export http_proxy={url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy"
+ .format(url=c['url'], port=c['port']))
+ else:
+ return str("export http_proxy=http://{usr}:{passwd}@{url}:{port}\nexport https_proxy=$http_proxy\nexport ftp_proxy=$http_proxy"
+ .format(url=re.sub('http://', '', c['url']), port=c['port'], usr=c['usr'], passwd=c['passwd']))
+
+
+def apply(ln):
+ if not ln and os.path.exists(proxy_def):
+ os.remove(proxy_def)
+ else:
+ open(proxy_def, 'w').write(
+ "# generated by system-proxy.py\n{}\n".format(ln))
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ ln = generate(c)
+ apply(ln)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)
diff --git a/src/conf_mode/system-syslog.py b/src/conf_mode/system-syslog.py
new file mode 100755
index 000000000..cfc1ca55f
--- /dev/null
+++ b/src/conf_mode/system-syslog.py
@@ -0,0 +1,259 @@
+#!/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():
+ 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 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)
+
+ # 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)
+
+
+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-timezone.py b/src/conf_mode/system-timezone.py
new file mode 100755
index 000000000..0f4513122
--- /dev/null
+++ b/src/conf_mode/system-timezone.py
@@ -0,0 +1,57 @@
+#!/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 sys
+import os
+
+from copy import deepcopy
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import call
+
+from vyos import airbag
+airbag.enable()
+
+default_config_data = {
+ 'name': 'UTC'
+}
+
+def get_config():
+ tz = deepcopy(default_config_data)
+ conf = Config()
+ if conf.exists('system time-zone'):
+ tz['name'] = conf.return_value('system time-zone')
+
+ return tz
+
+def verify(tz):
+ pass
+
+def generate(tz):
+ pass
+
+def apply(tz):
+ call('/usr/bin/timedatectl set-timezone {}'.format(tz['name']))
+
+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/system-wifi-regdom.py b/src/conf_mode/system-wifi-regdom.py
new file mode 100755
index 000000000..30ea89098
--- /dev/null
+++ b/src/conf_mode/system-wifi-regdom.py
@@ -0,0 +1,87 @@
+#!/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 copy import deepcopy
+from sys import exit
+
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_80211_file='/etc/modprobe.d/cfg80211.conf'
+config_crda_file='/etc/default/crda'
+
+default_config_data = {
+ 'regdom' : '',
+ 'deleted' : False
+}
+
+def get_config():
+ regdom = deepcopy(default_config_data)
+ conf = Config()
+ base = ['system', 'wifi-regulatory-domain']
+
+ # Check if interface has been removed
+ if not conf.exists(base):
+ regdom['deleted'] = True
+ return regdom
+ else:
+ regdom['regdom'] = conf.return_value(base)
+
+ return regdom
+
+def verify(regdom):
+ if regdom['deleted']:
+ return None
+
+ if not regdom['regdom']:
+ raise ConfigError("Wireless regulatory domain is mandatory.")
+
+ return None
+
+def generate(regdom):
+ print("Changing the wireless regulatory domain requires a system reboot.")
+
+ if regdom['deleted']:
+ if os.path.isfile(config_80211_file):
+ os.unlink(config_80211_file)
+
+ if os.path.isfile(config_crda_file):
+ os.unlink(config_crda_file)
+
+ return None
+
+ render(config_80211_file, 'wifi/cfg80211.conf.tmpl', regdom)
+ render(config_crda_file, 'wifi/crda.tmpl', regdom)
+ return None
+
+def apply(regdom):
+ 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_console.py b/src/conf_mode/system_console.py
new file mode 100755
index 000000000..6f83335f3
--- /dev/null
+++ b/src/conf_mode/system_console.py
@@ -0,0 +1,141 @@
+#!/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
+import re
+
+from fileinput import input as replace_in_file
+from vyos.config import Config
+from vyos.util import call
+from vyos.template import render
+from vyos import ConfigError, airbag
+airbag.enable()
+
+by_bus_dir = '/dev/serial/by-bus'
+
+def get_config():
+ conf = Config()
+ base = ['system', 'console']
+
+ # retrieve configuration at once
+ console = conf.get_config_dict(base, get_first_key=True)
+
+ # bail out early if no serial console is configured
+ if 'device' not in console.keys():
+ return console
+
+ # convert CLI values to system values
+ for device in console['device'].keys():
+ # no speed setting has been configured - use default value
+ if not 'speed' in console['device'][device].keys():
+ tmp = { 'speed': '' }
+ if device.startswith('hvc'):
+ tmp['speed'] = 38400
+ else:
+ tmp['speed'] = 115200
+
+ console['device'][device].update(tmp)
+
+ if device.startswith('usb'):
+ # It is much easiert to work with the native ttyUSBn name when using
+ # getty, but that name may change across reboots - depending on the
+ # amount of connected devices. We will resolve the fixed device name
+ # to its dynamic device file - and create a new dict entry for it.
+ by_bus_device = f'{by_bus_dir}/{device}'
+ if os.path.isdir(by_bus_dir) and os.path.exists(by_bus_device):
+ tmp = os.path.basename(os.readlink(by_bus_device))
+ # updating the dict must come as last step in the loop!
+ console['device'][tmp] = console['device'].pop(device)
+
+ return console
+
+def verify(console):
+ return None
+
+def generate(console):
+ base_dir = '/etc/systemd/system'
+ # Remove all serial-getty configuration files in advance
+ for root, dirs, files in os.walk(base_dir):
+ for basename in files:
+ if 'serial-getty' in basename:
+ call(f'systemctl stop {basename}')
+ os.unlink(os.path.join(root, basename))
+
+ if not console:
+ return None
+
+ for device in console['device'].keys():
+ config_file = base_dir + f'/serial-getty@{device}.service'
+ getty_wants_symlink = base_dir + f'/getty.target.wants/serial-getty@{device}.service'
+
+ render(config_file, 'getty/serial-getty.service.tmpl', console['device'][device])
+ os.symlink(config_file, getty_wants_symlink)
+
+ # GRUB
+ # For existing serial line change speed (if necessary)
+ # Only applys to ttyS0
+ if 'ttyS0' not in console['device'].keys():
+ return None
+
+ speed = console['device']['ttyS0']['speed']
+ grub_config = '/boot/grub/grub.cfg'
+ if not os.path.isfile(grub_config):
+ return None
+
+ # stdin/stdout are redirected in replace_in_file(), thus print() is fine
+ p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$')
+ for line in replace_in_file(grub_config, inplace=True):
+ if line.startswith('serial --unit'):
+ line = f'serial --unit=0 --speed={speed}\n'
+ elif p.match(line):
+ line = '{},{}{}\n'.format(p.search(line)[1], speed, p.search(line)[2])
+
+ print(line, end='')
+
+ return None
+
+def apply(console):
+ # reset screen blanking
+ call('/usr/bin/setterm -blank 0 -powersave off -powerdown 0 -term linux </dev/tty1 >/dev/tty1 2>&1')
+
+ # Reload systemd manager configuration
+ call('systemctl daemon-reload')
+
+ if not console:
+ return None
+
+ if 'powersave' in console.keys():
+ # Configure screen blank powersaving on VGA console
+ call('/usr/bin/setterm -blank 15 -powersave powerdown -powerdown 60 -term linux </dev/tty1 >/dev/tty1 2>&1')
+
+ # Start getty process on configured serial interfaces
+ for device in console['device'].keys():
+ # Only start console if it exists on the running system. If a user
+ # detaches a USB serial console and reboots - it should not fail!
+ if os.path.exists(f'/dev/{device}'):
+ call(f'systemctl start serial-getty@{device}.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_lcd.py b/src/conf_mode/system_lcd.py
new file mode 100755
index 000000000..31a09252d
--- /dev/null
+++ b/src/conf_mode/system_lcd.py
@@ -0,0 +1,88 @@
+#!/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():
+ 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 config file for client lcdproc
+ render(lcdproc_conf, 'lcd/lcdproc.conf.tmpl', lcd, trim_blocks=True)
+
+ 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/task_scheduler.py b/src/conf_mode/task_scheduler.py
new file mode 100755
index 000000000..51d8684cb
--- /dev/null
+++ b/src/conf_mode/task_scheduler.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2017 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
+import sys
+
+from vyos.config import Config
+from vyos import ConfigError
+
+from vyos import airbag
+airbag.enable()
+
+crontab_file = "/etc/cron.d/vyos-crontab"
+
+
+def format_task(minute="*", hour="*", day="*", dayofweek="*", month="*", user="root", rawspec=None, command=""):
+ fmt_full = "{minute} {hour} {day} {month} {dayofweek} {user} {command}\n"
+ fmt_raw = "{spec} {user} {command}\n"
+
+ if rawspec is None:
+ s = fmt_full.format(minute=minute, hour=hour, day=day,
+ dayofweek=dayofweek, month=month, command=command, user=user)
+ else:
+ s = fmt_raw.format(spec=rawspec, user=user, command=command)
+
+ return s
+
+def split_interval(s):
+ result = re.search(r"(\d+)([mdh]?)", s)
+ value = int(result.group(1))
+ suffix = result.group(2)
+ return( (value, suffix) )
+
+def make_command(executable, arguments):
+ if arguments:
+ return("sg vyattacfg \"{0} {1}\"".format(executable, arguments))
+ else:
+ return("sg vyattacfg \"{0}\"".format(executable))
+
+def get_config():
+ conf = Config()
+ conf.set_level("system task-scheduler task")
+ task_names = conf.list_nodes("")
+ tasks = []
+
+ for name in task_names:
+ interval = conf.return_value("{0} interval".format(name))
+ spec = conf.return_value("{0} crontab-spec".format(name))
+ executable = conf.return_value("{0} executable path".format(name))
+ args = conf.return_value("{0} executable arguments".format(name))
+ task = {
+ "name": name,
+ "interval": interval,
+ "spec": spec,
+ "executable": executable,
+ "args": args
+ }
+ tasks.append(task)
+
+ return tasks
+
+def verify(tasks):
+ for task in tasks:
+ if not task["interval"] and not task["spec"]:
+ raise ConfigError("Invalid task {0}: must define either interval or crontab-spec".format(task["name"]))
+
+ if task["interval"]:
+ if task["spec"]:
+ raise ConfigError("Invalid task {0}: cannot use interval and crontab-spec at the same time".format(task["name"]))
+
+ if not re.match(r"^\d+[mdh]?$", task["interval"]):
+ raise(ConfigError("Invalid interval {0} in task {1}: interval should be a number optionally followed by m, h, or d".format(task["name"], task["interval"])))
+ else:
+ # Check if values are within allowed range
+ value, suffix = split_interval(task["interval"])
+
+ if not suffix or suffix == "m":
+ if value > 60:
+ raise ConfigError("Invalid task {0}: interval in minutes must not exceed 60".format(task["name"]))
+ elif suffix == "h":
+ if value > 24:
+ raise ConfigError("Invalid task {0}: interval in hours must not exceed 24".format(task["name"]))
+ elif suffix == "d":
+ if value > 31:
+ raise ConfigError("Invalid task {0}: interval in days must not exceed 31".format(task["name"]))
+
+ if not task["executable"]:
+ raise ConfigError("Invalid task {0}: executable is not defined".format(task["name"]))
+ else:
+ # Check if executable exists and is executable
+ if not (os.path.isfile(task["executable"]) and os.access(task["executable"], os.X_OK)):
+ raise ConfigError("Invalid task {0}: file {1} does not exist or is not executable".format(task["name"], task["executable"]))
+
+def generate(tasks):
+ crontab_header = "### Generated by vyos-update-crontab.py ###\n"
+ if len(tasks) == 0:
+ if os.path.exists(crontab_file):
+ os.remove(crontab_file)
+ else:
+ pass
+ else:
+ crontab_lines = []
+ for task in tasks:
+ command = make_command(task["executable"], task["args"])
+ if task["spec"]:
+ line = format_task(command=command, rawspec=task["spec"])
+ else:
+ value, suffix = split_interval(task["interval"])
+ if not suffix or suffix == "m":
+ line = format_task(command=command, minute="*/{0}".format(value))
+ elif suffix == "h":
+ line = format_task(command=command, minute="0", hour="*/{0}".format(value))
+ elif suffix == "d":
+ line = format_task(command=command, minute="0", hour="0", day="*/{0}".format(value))
+ crontab_lines.append(line)
+
+ with open(crontab_file, 'w') as f:
+ f.write(crontab_header)
+ f.writelines(crontab_lines)
+
+def apply(config):
+ # No daemon restarts etc. needed for cron
+ pass
+
+
+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/tftp_server.py b/src/conf_mode/tftp_server.py
new file mode 100755
index 000000000..d31851bef
--- /dev/null
+++ b/src/conf_mode/tftp_server.py
@@ -0,0 +1,149 @@
+#!/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.validate import is_ipv4, is_addr_assigned
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/default/tftpd'
+
+default_config_data = {
+ 'directory': '',
+ 'allow_upload': False,
+ 'port': '69',
+ 'listen': []
+}
+
+def get_config():
+ tftpd = deepcopy(default_config_data)
+ conf = Config()
+ base = ['service', 'tftp-server']
+ if not conf.exists(base):
+ return None
+ else:
+ conf.set_level(base)
+
+ if conf.exists(['directory']):
+ tftpd['directory'] = conf.return_value(['directory'])
+
+ if conf.exists(['allow-upload']):
+ tftpd['allow_upload'] = True
+
+ if conf.exists(['port']):
+ tftpd['port'] = conf.return_value(['port'])
+
+ if conf.exists(['listen-address']):
+ tftpd['listen'] = conf.return_values(['listen-address'])
+
+ return tftpd
+
+def verify(tftpd):
+ # bail out early - looks like removal from running config
+ if tftpd is None:
+ return None
+
+ # Configuring allowed clients without a server makes no sense
+ if not tftpd['directory']:
+ raise ConfigError('TFTP root directory must be configured!')
+
+ if not tftpd['listen']:
+ raise ConfigError('TFTP server listen address must be configured!')
+
+ for addr in tftpd['listen']:
+ if not is_addr_assigned(addr):
+ print('WARNING: TFTP server listen address {0} not assigned to any interface!'.format(addr))
+
+ 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 listen in tftpd['listen']:
+ config = deepcopy(tftpd)
+ if is_ipv4(listen):
+ config['listen'] = [listen + ":" + tftpd['port'] + " -4"]
+ else:
+ config['listen'] = ["[" + listen + "]" + tftpd['port'] + " -6"]
+
+ file = config_file + str(idx)
+ 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@{0..20}.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)
+ os.chmod(tftp_root, stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH)
+
+ # 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 listen in tftpd['listen']:
+ call('systemctl restart tftpd@{0}.service'.format(idx))
+ 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_anyconnect.py b/src/conf_mode/vpn_anyconnect.py
new file mode 100755
index 000000000..158e1a117
--- /dev/null
+++ b/src/conf_mode/vpn_anyconnect.py
@@ -0,0 +1,135 @@
+#!/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', 'anyconnect']
+ if not conf.exists(base):
+ return None
+
+ ocserv = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
+ default_values = defaults(base)
+ ocserv = dict_merge(default_values, 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('Anyconect 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('anyconnect authentication mode required')
+ else:
+ raise ConfigError('anyconnect 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('anyconnect ssl {0} required'.format(cert.replace('_', '-')))
+ else:
+ raise ConfigError('anyconnect 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"
+ else:
+ raise ConfigError('anyconnect 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 servers
+ render(radius_servers, 'ocserv/radius_servers.tmpl', ocserv["authentication"]["radius"], trim_blocks=True)
+ 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 config
+ render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv, trim_blocks=True)
+
+
+
+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_l2tp.py b/src/conf_mode/vpn_l2tp.py
new file mode 100755
index 000000000..26ad1af84
--- /dev/null
+++ b/src/conf_mode/vpn_l2tp.py
@@ -0,0 +1,381 @@
+#!/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.util import call, get_half_cpus
+from vyos.validate import is_ipv4
+from vyos import ConfigError
+from vyos.template import render
+
+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_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():
+ 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', 'protocols']):
+ 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', 'protocols']):
+ 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(['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-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)
+
+ 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_pptp.py b/src/conf_mode/vpn_pptp.py
new file mode 100755
index 000000000..32cbadd74
--- /dev/null
+++ b/src/conf_mode/vpn_pptp.py
@@ -0,0 +1,286 @@
+#!/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_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():
+ 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(['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-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)
+
+ if pptp['local_users']:
+ render(pptp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', pptp, trim_blocks=True)
+ 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
new file mode 100755
index 000000000..ddb499bf4
--- /dev/null
+++ b/src/conf_mode/vpn_sstp.py
@@ -0,0 +1,387 @@
+#!/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 time import sleep
+from sys import exit
+from copy import deepcopy
+from stat import S_IRUSR, S_IWUSR, S_IRGRP
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.util import call, run, get_half_cpus
+from vyos.validate import is_ipv4
+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'
+
+default_config_data = {
+ 'local_users' : [],
+ 'auth_mode' : 'local',
+ 'auth_proto' : ['auth_mschap_v2'],
+ 'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template
+ 'client_ip_pool' : [],
+ 'client_ipv6_pool': [],
+ 'client_ipv6_delegate_prefix': [],
+ 'client_gateway': '',
+ 'dnsv4' : [],
+ 'dnsv6' : [],
+ 'radius_server' : [],
+ 'radius_acct_tmo' : '3',
+ 'radius_max_try' : '3',
+ 'radius_timeout' : '3',
+ 'radius_nas_id' : '',
+ 'radius_nas_ip' : '',
+ 'radius_source_address' : '',
+ 'radius_shaper_attr' : '',
+ 'radius_shaper_vendor': '',
+ 'radius_dynamic_author' : '',
+ 'ssl_ca' : '',
+ 'ssl_cert' : '',
+ 'ssl_key' : '',
+ 'mtu' : '',
+ 'ppp_mppe' : 'prefer',
+ 'ppp_echo_failure' : '',
+ 'ppp_echo_interval' : '',
+ 'ppp_echo_timeout' : '',
+ 'thread_cnt' : get_half_cpus()
+}
+
+def get_config():
+ sstp = deepcopy(default_config_data)
+ base_path = ['vpn', 'sstp']
+ conf = Config()
+ if not conf.exists(base_path):
+ return None
+
+ conf.set_level(base_path)
+
+ if conf.exists(['authentication', 'mode']):
+ sstp['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' : '*',
+ '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'])
+
+ sstp['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(['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']):
+ sstp['radius_server'].append(radius)
+
+ #
+ # advanced radius-setting
+ conf.set_level(base_path + ['authentication', 'radius'])
+
+ if conf.exists(['acct-timeout']):
+ sstp['radius_acct_tmo'] = conf.return_value(['acct-timeout'])
+
+ if conf.exists(['max-try']):
+ sstp['radius_max_try'] = conf.return_value(['max-try'])
+
+ if conf.exists(['timeout']):
+ sstp['radius_timeout'] = conf.return_value(['timeout'])
+
+ if conf.exists(['nas-identifier']):
+ sstp['radius_nas_id'] = conf.return_value(['nas-identifier'])
+
+ if conf.exists(['nas-ip-address']):
+ sstp['radius_nas_ip'] = conf.return_value(['nas-ip-address'])
+
+ if conf.exists(['source-address']):
+ sstp['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'])
+
+ sstp['radius_dynamic_author'] = dae
+
+ if conf.exists(['rate-limit', 'enable']):
+ sstp['radius_shaper_attr'] = 'Filter-Id'
+ c_attr = ['rate-limit', 'enable', 'attribute']
+ if conf.exists(c_attr):
+ sstp['radius_shaper_attr'] = conf.return_value(c_attr)
+
+ c_vendor = ['rate-limit', 'enable', 'vendor']
+ if conf.exists(c_vendor):
+ sstp['radius_shaper_vendor'] = conf.return_value(c_vendor)
+
+ #
+ # authentication protocols
+ conf.set_level(base_path + ['authentication'])
+ if conf.exists(['protocols']):
+ # clear default list content, now populate with actual CLI values
+ sstp['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(['protocols']):
+ sstp['auth_proto'].append(auth_mods[proto])
+
+ #
+ # read in SSL certs
+ conf.set_level(base_path + ['ssl'])
+ if conf.exists(['ca-cert-file']):
+ sstp['ssl_ca'] = conf.return_value(['ca-cert-file'])
+
+ if conf.exists(['cert-file']):
+ sstp['ssl_cert'] = conf.return_value(['cert-file'])
+
+ if conf.exists(['key-file']):
+ sstp['ssl_key'] = conf.return_value(['key-file'])
+
+
+ #
+ # read in client IPv4 pool
+ conf.set_level(base_path + ['network-settings', 'client-ip-settings'])
+ if conf.exists(['subnet']):
+ sstp['client_ip_pool'] = conf.return_values(['subnet'])
+
+ if conf.exists(['gateway-address']):
+ sstp['client_gateway'] = conf.return_value(['gateway-address'])
+
+ #
+ # read in client IPv6 pool
+ conf.set_level(base_path + ['network-settings', 'client-ipv6-pool'])
+ if conf.exists(['prefix']):
+ for prefix in conf.list_nodes(['prefix']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': '64'
+ }
+
+ if conf.exists(['prefix', prefix, 'mask']):
+ tmp['mask'] = conf.return_value(['prefix', prefix, 'mask'])
+
+ sstp['client_ipv6_pool'].append(tmp)
+
+ if conf.exists(['delegate']):
+ for prefix in conf.list_nodes(['delegate']):
+ tmp = {
+ 'prefix': prefix,
+ 'mask': ''
+ }
+
+ if conf.exists(['delegate', prefix, 'delegation-prefix']):
+ tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix'])
+
+ sstp['client_ipv6_delegate_prefix'].append(tmp)
+
+ #
+ # read in network settings
+ conf.set_level(base_path + ['network-settings'])
+ if conf.exists(['name-server']):
+ for name_server in conf.return_values(['name-server']):
+ if is_ipv4(name_server):
+ sstp['dnsv4'].append(name_server)
+ else:
+ sstp['dnsv6'].append(name_server)
+
+ if conf.exists(['mtu']):
+ sstp['mtu'] = conf.return_value(['mtu'])
+
+ #
+ # read in PPP stuff
+ conf.set_level(base_path + ['ppp-settings'])
+ if conf.exists('mppe'):
+ sstp['ppp_mppe'] = conf.return_value(['ppp-settings', 'mppe'])
+
+ if conf.exists(['lcp-echo-failure']):
+ sstp['ppp_echo_failure'] = conf.return_value(['lcp-echo-failure'])
+
+ if conf.exists(['lcp-echo-interval']):
+ sstp['ppp_echo_interval'] = conf.return_value(['lcp-echo-interval'])
+
+ if conf.exists(['lcp-echo-timeout']):
+ sstp['ppp_echo_timeout'] = conf.return_value(['lcp-echo-timeout'])
+
+ return sstp
+
+
+def verify(sstp):
+ if sstp is None:
+ return None
+
+ # vertify auth settings
+ if sstp['auth_mode'] == 'local':
+ if not sstp['local_users']:
+ raise ConfigError('SSTP local auth mode requires local users to be configured!')
+
+ for user in sstp['local_users']:
+ username = user['name']
+ if not user['password']:
+ raise ConfigError(f'Password required for local user "{username}"')
+
+ # if up/download is set, check that both have a value
+ if user['upload'] and not user['download']:
+ raise ConfigError(f'Download speed value required for local user "{username}"')
+
+ if user['download'] and not user['upload']:
+ raise ConfigError(f'Upload speed value required for local user "{username}"')
+
+ if not sstp['client_ip_pool']:
+ raise ConfigError('Client IP subnet required')
+
+ if not sstp['client_gateway']:
+ raise ConfigError('Client gateway IP address required')
+
+ if len(sstp['dnsv4']) > 2:
+ raise ConfigError('Not more then two IPv4 DNS name-servers can be configured')
+
+ # check ipv6
+ if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']:
+ raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix')
+
+ for prefix in sstp['client_ipv6_delegate_prefix']:
+ if not prefix['mask']:
+ raise ConfigError('Delegation-prefix required for individual delegated networks')
+
+ if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:
+ raise ConfigError('One or more SSL certificates missing')
+
+ if not os.path.exists(sstp['ssl_ca']):
+ file = sstp['ssl_ca']
+ raise ConfigError(f'SSL CA certificate file "{file}" does not exist')
+
+ if not os.path.exists(sstp['ssl_cert']):
+ file = sstp['ssl_cert']
+ raise ConfigError(f'SSL public key file "{file}" does not exist')
+
+ if not os.path.exists(sstp['ssl_key']):
+ file = sstp['ssl_key']
+ raise ConfigError(f'SSL private key file "{file}" does not exist')
+
+ if sstp['auth_mode'] == 'radius':
+ if len(sstp['radius_server']) == 0:
+ raise ConfigError('RADIUS authentication requires at least one server')
+
+ for radius in sstp['radius_server']:
+ if not radius['key']:
+ server = radius['server']
+ raise ConfigError(f'Missing RADIUS secret key for server "{ server }"')
+
+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)
+
+ if sstp['local_users']:
+ render(sstp_chap_secrets, 'accel-ppp/chap-secrets.tmpl', sstp, trim_blocks=True)
+ os.chmod(sstp_chap_secrets, S_IRUSR | S_IWUSR | S_IRGRP)
+ 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/conf_mode/vrf.py b/src/conf_mode/vrf.py
new file mode 100755
index 000000000..56ca813ff
--- /dev/null
+++ b/src/conf_mode/vrf.py
@@ -0,0 +1,269 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+from copy import deepcopy
+from json import loads
+
+from vyos.config import Config
+from vyos.configdict import list_diff
+from vyos.ifconfig import Interface
+from vyos.util import read_file, cmd
+from vyos import ConfigError
+from vyos.template import render
+
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/etc/iproute2/rt_tables.d/vyos-vrf.conf'
+
+default_config_data = {
+ 'bind_to_all': '0',
+ 'deleted': False,
+ 'vrf_add': [],
+ 'vrf_existing': [],
+ 'vrf_remove': []
+}
+
+def _cmd(command):
+ cmd(command, raising=ConfigError, message='Error changing VRF')
+
+def list_rules():
+ command = 'ip -j -4 rule show'
+ answer = loads(cmd(command))
+ return [_ for _ in answer if _]
+
+def vrf_interfaces(c, match):
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['interfaces'])
+ section = c.get_config_dict([], get_first_key=True)
+ for type in section:
+ interfaces = section[type]
+ for name in interfaces:
+ interface = interfaces[name]
+ if 'vrf' in interface:
+ v = interface.get('vrf', '')
+ if v == match:
+ matched.append(name)
+
+ c.set_level(old_level)
+ return matched
+
+def vrf_routing(c, match):
+ matched = []
+ old_level = c.get_level()
+ c.set_level(['protocols', 'vrf'])
+ if match in c.list_nodes([]):
+ matched.append(match)
+
+ c.set_level(old_level)
+ return matched
+
+
+def get_config():
+ conf = Config()
+ vrf_config = deepcopy(default_config_data)
+
+ cfg_base = ['vrf']
+ if not conf.exists(cfg_base):
+ # get all currently effetive VRFs and mark them for deletion
+ vrf_config['vrf_remove'] = conf.list_effective_nodes(cfg_base + ['name'])
+ else:
+ # set configuration level base
+ conf.set_level(cfg_base)
+
+ # Should services be allowed to bind to all VRFs?
+ if conf.exists(['bind-to-all']):
+ vrf_config['bind_to_all'] = '1'
+
+ # Determine vrf interfaces (currently effective) - to determine which
+ # vrf interface is no longer present and needs to be removed
+ eff_vrf = conf.list_effective_nodes(['name'])
+ act_vrf = conf.list_nodes(['name'])
+ vrf_config['vrf_remove'] = list_diff(eff_vrf, act_vrf)
+
+ # read in individual VRF definition and build up
+ # configuration
+ for name in conf.list_nodes(['name']):
+ vrf_inst = {
+ 'description' : '',
+ 'members': [],
+ 'name' : name,
+ 'table' : '',
+ 'table_mod': False
+ }
+ conf.set_level(cfg_base + ['name', name])
+
+ if conf.exists(['table']):
+ # VRF table can't be changed on demand, thus we need to read in the
+ # current and the effective routing table number
+ act_table = conf.return_value(['table'])
+ eff_table = conf.return_effective_value(['table'])
+ vrf_inst['table'] = act_table
+ if eff_table and eff_table != act_table:
+ vrf_inst['table_mod'] = True
+
+ if conf.exists(['description']):
+ vrf_inst['description'] = conf.return_value(['description'])
+
+ # append individual VRF configuration to global configuration list
+ vrf_config['vrf_add'].append(vrf_inst)
+
+ # set configuration level base
+ conf.set_level(cfg_base)
+
+ # check VRFs which need to be removed as they are not allowed to have
+ # interfaces attached
+ tmp = []
+ for name in vrf_config['vrf_remove']:
+ vrf_inst = {
+ 'interfaces': [],
+ 'name': name,
+ 'routes': []
+ }
+
+ # find member interfaces of this particulat VRF
+ vrf_inst['interfaces'] = vrf_interfaces(conf, name)
+
+ # find routing protocols used by this VRF
+ vrf_inst['routes'] = vrf_routing(conf, name)
+
+ # append individual VRF configuration to temporary configuration list
+ tmp.append(vrf_inst)
+
+ # replace values in vrf_remove with list of dictionaries
+ # as we need it in verify() - we can't delete a VRF with members attached
+ vrf_config['vrf_remove'] = tmp
+ return vrf_config
+
+def verify(vrf_config):
+ # ensure VRF is not assigned to any interface
+ for vrf in vrf_config['vrf_remove']:
+ if len(vrf['interfaces']) > 0:
+ raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active member interfaces!")
+
+ if len(vrf['routes']) > 0:
+ raise ConfigError(f"VRF {vrf['name']} can not be deleted. It has active routing protocols!")
+
+ table_ids = []
+ for vrf in vrf_config['vrf_add']:
+ # table id is mandatory
+ if not vrf['table']:
+ raise ConfigError(f"VRF {vrf['name']} table id is mandatory!")
+
+ # routing table id can't be changed - OS restriction
+ if vrf['table_mod']:
+ raise ConfigError(f"VRF {vrf['name']} table id modification is not possible!")
+
+ # VRf routing table ID must be unique on the system
+ if vrf['table'] in table_ids:
+ raise ConfigError(f"VRF {vrf['name']} table id {vrf['table']} is not unique!")
+
+ table_ids.append(vrf['table'])
+
+ return None
+
+def generate(vrf_config):
+ render(config_file, 'vrf/vrf.conf.tmpl', vrf_config)
+ return None
+
+def apply(vrf_config):
+ # Documentation
+ #
+ # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt
+ # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF)
+ # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling
+ # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf
+ # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf
+
+ # set the default VRF global behaviour
+ bind_all = vrf_config['bind_to_all']
+ if read_file('/proc/sys/net/ipv4/tcp_l3mdev_accept') != bind_all:
+ _cmd(f'sysctl -wq net.ipv4.tcp_l3mdev_accept={bind_all}')
+ _cmd(f'sysctl -wq net.ipv4.udp_l3mdev_accept={bind_all}')
+
+ for vrf in vrf_config['vrf_remove']:
+ name = vrf['name']
+ if os.path.isdir(f'/sys/class/net/{name}'):
+ _cmd(f'ip -4 route del vrf {name} unreachable default metric 4278198272')
+ _cmd(f'ip -6 route del vrf {name} unreachable default metric 4278198272')
+ _cmd(f'ip link delete dev {name}')
+
+ for vrf in vrf_config['vrf_add']:
+ name = vrf['name']
+ table = vrf['table']
+
+ if not os.path.isdir(f'/sys/class/net/{name}'):
+ # For each VRF apart from your default context create a VRF
+ # interface with a separate routing table
+ _cmd(f'ip link add {name} type vrf table {table}')
+ # Start VRf
+ _cmd(f'ip link set dev {name} up')
+ # The kernel Documentation/networking/vrf.txt also recommends
+ # adding unreachable routes to the VRF routing tables so that routes
+ # afterwards are taken.
+ _cmd(f'ip -4 route add vrf {name} unreachable default metric 4278198272')
+ _cmd(f'ip -6 route add vrf {name} unreachable default metric 4278198272')
+
+ # set VRF description for e.g. SNMP monitoring
+ Interface(name).set_alias(vrf['description'])
+
+ # Linux routing uses rules to find tables - routing targets are then
+ # looked up in those tables. If the lookup got a matching route, the
+ # process ends.
+ #
+ # TL;DR; first table with a matching entry wins!
+ #
+ # You can see your routing table lookup rules using "ip rule", sadly the
+ # local lookup is hit before any VRF lookup. Pinging an addresses from the
+ # VRF will usually find a hit in the local table, and never reach the VRF
+ # routing table - this is usually not what you want. Thus we will
+ # re-arrange the tables and move the local lookup furhter down once VRFs
+ # are enabled.
+
+ # get current preference on local table
+ local_pref = [r.get('priority') for r in list_rules() if r.get('table') == 'local'][0]
+
+ # change preference when VRFs are enabled and local lookup table is default
+ if not local_pref and vrf_config['vrf_add']:
+ for af in ['-4', '-6']:
+ _cmd(f'ip {af} rule add pref 32765 table local')
+ _cmd(f'ip {af} rule del pref 0')
+
+ # return to default lookup preference when no VRF is configured
+ if not vrf_config['vrf_add']:
+ for af in ['-4', '-6']:
+ _cmd(f'ip {af} rule add pref 0 table local')
+ _cmd(f'ip {af} rule del pref 32765')
+
+ # clean out l3mdev-table rule if present
+ if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]:
+ _cmd(f'ip {af} rule del pref 1000')
+
+ 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/vrrp.py b/src/conf_mode/vrrp.py
new file mode 100755
index 000000000..292eb0c78
--- /dev/null
+++ b/src/conf_mode/vrrp.py
@@ -0,0 +1,256 @@
+#!/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 ipaddress import ip_address, ip_interface, IPv4Interface, IPv6Interface, IPv4Address, IPv6Address
+from json import dumps
+from pathlib import Path
+
+import vyos.config
+
+from vyos import ConfigError
+from vyos.util import call
+from vyos.template import render
+
+from vyos.ifconfig.vrrp import VRRP
+
+from vyos import airbag
+airbag.enable()
+
+def get_config():
+ vrrp_groups = []
+ sync_groups = []
+
+ config = vyos.config.Config()
+
+ # Get the VRRP groups
+ for group_name in config.list_nodes("high-availability vrrp group"):
+ config.set_level("high-availability vrrp group {0}".format(group_name))
+
+ # Retrieve the values
+ group = {"preempt": True, "use_vmac": False, "disable": False}
+
+ if config.exists("disable"):
+ group["disable"] = True
+
+ group["name"] = group_name
+ group["vrid"] = config.return_value("vrid")
+ group["interface"] = config.return_value("interface")
+ group["description"] = config.return_value("description")
+ group["advertise_interval"] = config.return_value("advertise-interval")
+ group["priority"] = config.return_value("priority")
+ group["hello_source"] = config.return_value("hello-source-address")
+ group["peer_address"] = config.return_value("peer-address")
+ group["sync_group"] = config.return_value("sync-group")
+ group["preempt_delay"] = config.return_value("preempt-delay")
+ group["virtual_addresses"] = config.return_values("virtual-address")
+
+ group["auth_password"] = config.return_value("authentication password")
+ group["auth_type"] = config.return_value("authentication type")
+
+ group["health_check_script"] = config.return_value("health-check script")
+ group["health_check_interval"] = config.return_value("health-check interval")
+ group["health_check_count"] = config.return_value("health-check failure-count")
+
+ group["master_script"] = config.return_value("transition-script master")
+ group["backup_script"] = config.return_value("transition-script backup")
+ group["fault_script"] = config.return_value("transition-script fault")
+ group["stop_script"] = config.return_value("transition-script stop")
+
+ if config.exists("no-preempt"):
+ group["preempt"] = False
+ if config.exists("rfc3768-compatibility"):
+ group["use_vmac"] = True
+
+ # Substitute defaults where applicable
+ if not group["advertise_interval"]:
+ group["advertise_interval"] = 1
+ if not group["priority"]:
+ group["priority"] = 100
+ if not group["preempt_delay"]:
+ group["preempt_delay"] = 0
+ if not group["health_check_interval"]:
+ group["health_check_interval"] = 60
+ if not group["health_check_count"]:
+ group["health_check_count"] = 3
+
+ # FIXUP: translate our option for auth type to keepalived's syntax
+ # for simplicity
+ if group["auth_type"]:
+ if group["auth_type"] == "plaintext-password":
+ group["auth_type"] = "PASS"
+ else:
+ group["auth_type"] = "AH"
+
+ vrrp_groups.append(group)
+
+ config.set_level("")
+
+ # Get the sync group used for conntrack-sync
+ conntrack_sync_group = None
+ if config.exists("service conntrack-sync failover-mechanism vrrp"):
+ conntrack_sync_group = config.return_value("service conntrack-sync failover-mechanism vrrp sync-group")
+
+ # Get the sync groups
+ for sync_group_name in config.list_nodes("high-availability vrrp sync-group"):
+ config.set_level("high-availability vrrp sync-group {0}".format(sync_group_name))
+
+ sync_group = {"conntrack_sync": False}
+ sync_group["name"] = sync_group_name
+ sync_group["members"] = config.return_values("member")
+ if conntrack_sync_group:
+ if conntrack_sync_group == sync_group_name:
+ sync_group["conntrack_sync"] = True
+
+ # add transition script configuration
+ sync_group["master_script"] = config.return_value("transition-script master")
+ sync_group["backup_script"] = config.return_value("transition-script backup")
+ sync_group["fault_script"] = config.return_value("transition-script fault")
+ sync_group["stop_script"] = config.return_value("transition-script stop")
+
+ sync_groups.append(sync_group)
+
+ # create a file with dict with proposed configuration
+ with open("{}.temp".format(VRRP.location['vyos']), 'w') as dict_file:
+ dict_file.write(dumps({'vrrp_groups': vrrp_groups, 'sync_groups': sync_groups}))
+
+ return (vrrp_groups, sync_groups)
+
+
+def verify(data):
+ vrrp_groups, sync_groups = data
+
+ for group in vrrp_groups:
+ # Check required fields
+ if not group["vrid"]:
+ raise ConfigError("vrid is required but not set in VRRP group {0}".format(group["name"]))
+ if not group["interface"]:
+ raise ConfigError("interface is required but not set in VRRP group {0}".format(group["name"]))
+ if not group["virtual_addresses"]:
+ raise ConfigError("virtual-address is required but not set in VRRP group {0}".format(group["name"]))
+
+ if group["auth_password"] and (not group["auth_type"]):
+ raise ConfigError("authentication type is required but not set in VRRP group {0}".format(group["name"]))
+
+ # Keepalived doesn't allow mixing IPv4 and IPv6 in one group, so we mirror that restriction
+
+ # XXX: filter on map object is destructive, so we force it to list.
+ # Additionally, filter objects always evaluate to True, empty or not,
+ # so we force them to lists as well.
+ vaddrs = list(map(lambda i: ip_interface(i), group["virtual_addresses"]))
+ vaddrs4 = list(filter(lambda x: isinstance(x, IPv4Interface), vaddrs))
+ vaddrs6 = list(filter(lambda x: isinstance(x, IPv6Interface), vaddrs))
+
+ if vaddrs4 and vaddrs6:
+ raise ConfigError("VRRP group {0} mixes IPv4 and IPv6 virtual addresses, this is not allowed. Create separate groups for IPv4 and IPv6".format(group["name"]))
+
+ if vaddrs4:
+ if group["hello_source"]:
+ hsa = ip_address(group["hello_source"])
+ if isinstance(hsa, IPv6Address):
+ raise ConfigError("VRRP group {0} uses IPv4 but its hello-source-address is IPv6".format(group["name"]))
+ if group["peer_address"]:
+ pa = ip_address(group["peer_address"])
+ if isinstance(pa, IPv6Address):
+ raise ConfigError("VRRP group {0} uses IPv4 but its peer-address is IPv6".format(group["name"]))
+
+ if vaddrs6:
+ if group["hello_source"]:
+ hsa = ip_address(group["hello_source"])
+ if isinstance(hsa, IPv4Address):
+ raise ConfigError("VRRP group {0} uses IPv6 but its hello-source-address is IPv4".format(group["name"]))
+ if group["peer_address"]:
+ pa = ip_address(group["peer_address"])
+ if isinstance(pa, IPv4Address):
+ raise ConfigError("VRRP group {0} uses IPv6 but its peer-address is IPv4".format(group["name"]))
+
+ # Disallow same VRID on multiple interfaces
+ _groups = sorted(vrrp_groups, key=(lambda x: x["interface"]))
+ count = len(_groups) - 1
+ index = 0
+ while (index < count):
+ if (_groups[index]["vrid"] == _groups[index + 1]["vrid"]) and (_groups[index]["interface"] == _groups[index + 1]["interface"]):
+ raise ConfigError("VRID {0} is used in groups {1} and {2} that both use interface {3}. Groups on the same interface must use different VRIDs".format(
+ _groups[index]["vrid"], _groups[index]["name"], _groups[index + 1]["name"], _groups[index]["interface"]))
+ else:
+ index += 1
+
+ # Check sync groups
+ vrrp_group_names = list(map(lambda x: x["name"], vrrp_groups))
+
+ for sync_group in sync_groups:
+ for m in sync_group["members"]:
+ if not (m in vrrp_group_names):
+ raise ConfigError("VRRP sync-group {0} refers to VRRP group {1}, but group {1} does not exist".format(sync_group["name"], m))
+
+
+def generate(data):
+ vrrp_groups, sync_groups = data
+
+ # Remove disabled groups from the sync group member lists
+ for sync_group in sync_groups:
+ for member in sync_group["members"]:
+ g = list(filter(lambda x: x["name"] == member, vrrp_groups))[0]
+ if g["disable"]:
+ print("Warning: ignoring disabled VRRP group {0} in sync-group {1}".format(g["name"], sync_group["name"]))
+ # Filter out disabled groups
+ vrrp_groups = list(filter(lambda x: x["disable"] is not True, vrrp_groups))
+
+ render(VRRP.location['config'], 'vrrp/keepalived.conf.tmpl',
+ {"groups": vrrp_groups, "sync_groups": sync_groups})
+ render(VRRP.location['daemon'], 'vrrp/daemon.tmpl', {})
+ return None
+
+
+def apply(data):
+ vrrp_groups, sync_groups = data
+ if vrrp_groups:
+ # safely rename a temporary file with configuration dict
+ try:
+ dict_file = Path("{}.temp".format(VRRP.location['vyos']))
+ dict_file.rename(Path(VRRP.location['vyos']))
+ except Exception as err:
+ print("Unable to rename the file with keepalived config for FIFO pipe: {}".format(err))
+
+ if not VRRP.is_running():
+ print("Starting the VRRP process")
+ ret = call("systemctl restart keepalived.service")
+ else:
+ print("Reloading the VRRP process")
+ ret = call("systemctl reload keepalived.service")
+
+ if ret != 0:
+ raise ConfigError("keepalived failed to start")
+ else:
+ # VRRP is removed in the commit
+ print("Stopping the VRRP process")
+ call("systemctl stop keepalived.service")
+ os.unlink(VRRP.location['daemon'])
+
+ return None
+
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print("VRRP error: {0}".format(str(e)))
+ exit(1)
diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py
new file mode 100755
index 000000000..fb4644d5a
--- /dev/null
+++ b/src/conf_mode/vyos_cert.py
@@ -0,0 +1,144 @@
+#!/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 sys
+import os
+import tempfile
+import pathlib
+import ssl
+
+import vyos.defaults
+from vyos.config import Config
+from vyos import ConfigError
+from vyos.util import cmd
+
+from vyos import airbag
+airbag.enable()
+
+vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode']
+
+# XXX: this model will need to be extended for tag nodes
+dependencies = [
+ 'https.py',
+]
+
+def status_self_signed(cert_data):
+# check existence and expiration date
+ path = pathlib.Path(cert_data['conf'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['crt'])
+ if not path.is_file():
+ return False
+ path = pathlib.Path(cert_data['key'])
+ if not path.is_file():
+ return False
+
+ # check if certificate is 1/2 past lifetime, with openssl -checkend
+ end_days = int(cert_data['lifetime'])
+ end_seconds = int(0.5*60*60*24*end_days)
+ checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data)
+ try:
+ cmd(checkend_cmd, message='Called process error')
+ return True
+ except OSError as err:
+ if err.errno == 1:
+ return False
+ print(err)
+ # XXX: This seems wrong to continue on failure
+ # implicitely returning None
+
+def generate_self_signed(cert_data):
+ san_config = None
+
+ if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0):
+ san_config = tempfile.NamedTemporaryFile()
+ with open(san_config.name, 'w') as fd:
+ fd.write('[req]\n')
+ fd.write('distinguished_name=req\n')
+ fd.write('[san]\n')
+ fd.write('subjectAltName=DNS:vyos\n')
+
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-extensions san -config {san_conf}'
+ ''.format(san_conf=san_config.name,
+ **cert_data))
+
+ else:
+ openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} '
+ '-newkey rsa:4096 -keyout {key} -out {crt} '
+ '-subj "/O=Sentrium/OU=VyOS/CN=vyos" '
+ '-addext "subjectAltName=DNS:vyos"'
+ ''.format(**cert_data))
+
+ try:
+ cmd(openssl_req_cmd, message='Called process error')
+ except OSError as err:
+ print(err)
+ # XXX: seems wrong to ignore the failure
+
+ os.chmod('{key}'.format(**cert_data), 0o400)
+
+ with open('{conf}'.format(**cert_data), 'w') as f:
+ f.write('ssl_certificate {crt};\n'.format(**cert_data))
+ f.write('ssl_certificate_key {key};\n'.format(**cert_data))
+
+ if san_config:
+ san_config.close()
+
+def get_config():
+ vyos_cert = vyos.defaults.vyos_cert_data
+
+ conf = Config()
+ if not conf.exists('service https certificates system-generated-certificate'):
+ return None
+ else:
+ conf.set_level('service https certificates system-generated-certificate')
+
+ if conf.exists('lifetime'):
+ lifetime = conf.return_value('lifetime')
+ vyos_cert['lifetime'] = lifetime
+
+ return vyos_cert
+
+def verify(vyos_cert):
+ return None
+
+def generate(vyos_cert):
+ if vyos_cert is None:
+ return None
+
+ if not status_self_signed(vyos_cert):
+ generate_self_signed(vyos_cert)
+
+def apply(vyos_cert):
+ for dep in dependencies:
+ command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep)
+ cmd(command, raising=ConfigError)
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)