From ab5ca2796c1aad0043cc0db80299e4e2d42c1b22 Mon Sep 17 00:00:00 2001 From: hagbard Date: Thu, 25 Jul 2019 09:48:08 -0700 Subject: [accel-l2tp] - T834: l2tp implementation - node.def deletion for show remote-access - IPSec interface checking for L2TP - IPSec x509 for l2tp - verification of outside-address to warning since it was optional in the previous config --- Makefile | 1 + interface-definitions/l2tp-server.xml | 507 ++++++++++++++++++++++++++++++++++ op-mode-definitions/l2tp-server.xml | 20 ++ op-mode-definitions/show-vpn.xml | 20 ++ src/conf_mode/accel_l2tp.py | 466 +++++++++++++++++++++++++++++++ src/conf_mode/ipsec-settings.py | 197 ++++++++++++- src/op_mode/show_vpn_ra.py | 58 ++++ 7 files changed, 1267 insertions(+), 2 deletions(-) create mode 100644 interface-definitions/l2tp-server.xml create mode 100644 op-mode-definitions/l2tp-server.xml create mode 100644 op-mode-definitions/show-vpn.xml create mode 100755 src/conf_mode/accel_l2tp.py create mode 100755 src/op_mode/show_vpn_ra.py diff --git a/Makefile b/Makefile index 2e49a204c..063e9b009 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ op_mode_definitions: rm -f $(OP_TMPL_DIR)/restart/node.def rm -f $(OP_TMPL_DIR)/monitor/node.def rm -f $(OP_TMPL_DIR)/generate/node.def + rm -f $(OP_TMPL_DIR)/show/vpn/node.def .PHONY: all all: clean interface_definitions op_mode_definitions diff --git a/interface-definitions/l2tp-server.xml b/interface-definitions/l2tp-server.xml new file mode 100644 index 000000000..2d103aae0 --- /dev/null +++ b/interface-definitions/l2tp-server.xml @@ -0,0 +1,507 @@ + + + + + + + L2TP Virtual Private Network (VPN) + + + + + Remote access L2TP VPN + + + + + Maximum Transmission Unit (MTU) + + + + + + + + External IP address to which VPN clients will connect + + + + + + + + Nexthop IP address for reaching the VPN clients + + + + + + + + IPv4 Domain Name Service (DNS) server + + + + + Primary DNS server + + ipv4 + IPv4 address + + + + + + + + + Secondary DNS server + + ipv4 + IPv4 address + + + + + + + + + + + Internet Protocol Security (IPsec) for remote access L2TP VPN + + + + + IPsec authentication settings + + + + + Authentication mode for IPsec + + pre-shared-secret + Use pre-shared secret for IPsec authentication + + + x509 + Use X.509 certificate for IPsec authentication + + + ^(pre-shared-secret|x509) + + + pre-shared-secret x509 + + + + + + Pre-shared secret for IPsec + + + + + X.509 certificate + + + + + File containing the X.509 certificate for the Certificate Authority (CA) + + <text> + File in /config/auth + + + + + + File containing the X.509 Certificate Revocation List (CRL) + + <text> + File in /config/auth + + + + + + File containing the X.509 certificate for the remote access VPN server (this host) + + <text> + File in /config/auth + + + + + + File containing the private key for the X.509 certificate for the remote access VPN server (this host) + + <text> + File in /config/auth + + + + + + Password that protects the private key + + + + + + + + + IKE lifetime + + <30-86400> + IKE lifetime in seconds (default 3600) + + + + + + + + + ESP lifetime + + <30-86400> + IKE lifetime in seconds (default 3600) + + + + + + + + + + + Windows Internet Name Service (WINS) server settings + + + + + Primary WINS server + + + + + + + + Secondary WINS server + + + + + + + + + + Pool of client IP addresses (must be within a /24) + + + + + First IP address in the pool (will be used as gateway address) + + + + + + + + Last IP address in the pool + + + + + + + + Client IP subnet (CIDR notation) + + + + Not a valid CIDR formatted prefix + + ipv4net + IPv4 subnet address + + + + + + + + + Description for L2TP remote-access settings + + + + + DHCP interface to listen on + + + + + PPP idle timeout + + <30-86400> + PPP idle timeout in seconds (default 1800) + + + + + + + + + Authentication for remote access L2TP VPN + + + + + Authentication protocol for remote access peer L2TP VPN + + pap + Require the peer to authenticate itself using PAP [Password Authentication Protocol]. + + + chap + Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol]. + + + mschap + Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol]. + + + mschap-v2 + Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2]. + + + ^(pap|chap|mschap|mschap-v2) + + + pap chap mschap mschap-v2 + + + + + + + Specifies mppe negotioation preference. (default require mppe 128-bit stateless + + deny + deny mppe + + + prefer + ask client for mppe, if it rejects don't fail + + + require + ask client for mppe, if it rejects drop connection + + + ^(deny|prefer|require) + + + deny prefer require + + + + + + Authentication mode for remote access L2TP VPN + + local + Use local username/password configuration + + + radius + Use a RADIUS server to autenticate users + + + ^(local|radius) + + + local radius + + + + + + Local user authentication for remote access L2TP VPN + + + + + User name for authentication + + + + + Option to disable a L2TP Server user + + + + + Password for authentication + + + + + Static client IP address + + + + + Upload/Download speed limits + + + + + Upload bandwidth limit in kbits/sec + + + + + + + + Download bandwidth limit in kbits/sec + + + + + + + + + + + + + + RADIUS specific configuration + + + + + IP address of radius server + + ipv4 + IP address of RADIUS server + + + + + + Key for accessing the specified server + + + + + Maximum number of simultaneous requests to server (default: unlimited) + + + + + If server doesn't responds mark it as unavailable for this amount of time in seconds + + + + + + + + + RADIUS settings + + + + + Timeout to wait response from server (seconds) + + + + + Timeout to wait reply for Interim-Update packets. (default 3 seconds) + + + + + Maximum number of tries to send Access-Request/Accounting-Request queries + + + + + Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests. + + + + + Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address. + + + + + IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA) + + + + + IP address for Dynamic Authorization Extension server (DM/CoA) + + + + + Port for Dynamic Authorization Extension server (DM/CoA) + + + + + Secret for Dynamic Authorization Extension server (DM/CoA) + + + + + + + Upload/Download speed limits + + + + + Specifies which radius attribute contains rate information. (default is Filter-Id) + + + + + Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius) + + + + + Enables Bandwidth shaping via RADIUS + + + + + + + + + + + + + + + + diff --git a/op-mode-definitions/l2tp-server.xml b/op-mode-definitions/l2tp-server.xml new file mode 100644 index 000000000..fb1b85ce4 --- /dev/null +++ b/op-mode-definitions/l2tp-server.xml @@ -0,0 +1,20 @@ + + + + + + + show l2tp-server status + + + + + Show active L2TP server sessions + + /usr/bin/accel-cmd -p 2004 'show sessions' + + + + + + diff --git a/op-mode-definitions/show-vpn.xml b/op-mode-definitions/show-vpn.xml new file mode 100644 index 000000000..0e7fc38e9 --- /dev/null +++ b/op-mode-definitions/show-vpn.xml @@ -0,0 +1,20 @@ + + + + + + + Show active remote access Virtual Private Network (VPN) sessions + + + + + Show active VPN server sessions + + ${vyos_op_scripts_dir}/show_vpn_ra.py + + + + + + diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py new file mode 100755 index 000000000..39732b97d --- /dev/null +++ b/src/conf_mode/accel_l2tp.py @@ -0,0 +1,466 @@ +#!/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 . +# +# + +import sys +import os +import re +import subprocess +import jinja2 +import socket +import time +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError + +pidfile = r'/var/run/accel_l2tp.pid' +l2tp_cnf_dir = r'/etc/accel-ppp/l2tp' +chap_secrets = l2tp_cnf_dir + '/chap-secrets' +l2tp_conf = l2tp_cnf_dir + '/l2tp.config' +# accel-pppd -d -c /etc/accel-ppp/l2tp/l2tp.config -p /var/run/accel_l2tp.pid + +### config path creation +if not os.path.exists(l2tp_cnf_dir): + os.makedirs(l2tp_cnf_dir) + sl.syslog(sl.LOG_NOTICE, l2tp_cnf_dir + " created") + +l2tp_config = ''' +### generated by accel_l2tp.py ### +[modules] +log_syslog +l2tp +chap-secrets +{% for proto in authentication['auth_proto']: %} +{{proto}} +{% endfor%} +{% if authentication['mode'] == 'radius' %} +radius +{% endif -%} +ippool +shaper + +[core] +thread-count={{thread_cnt}} + +[log] +syslog=accel-l2tp,daemon +copy=1 +level=5 + +{% if dns %} +[dns] +{% if dns[0] %} +dns1={{dns[0]}} +{% endif %} +{% if dns[1] %} +dns2={{dns[1]}} +{% endif %} +{% endif -%} + +{% if wins %} +[wins] +{% if wins[0] %} +wins1={{wins[0]}} +{% endif %} +{% if wins[1] %} +wins2={{wins[1]}} +{% endif %} +{% endif -%} + +[l2tp] +verbose=1 +ppp-max-mtu={{mtu}} +mppe={{authentication['mppe']}} +{% if outside_addr %} +bind={{outside_addr}} +{% endif %} + +[client-ip-range] +0.0.0.0/0 + +{% if (client_ip_pool) or (client_ip_subnets) %} +[ip-pool] +{% if client_ip_pool %} +{{client_ip_pool}} +{% endif -%} +{% if client_ip_subnets %} +{% for sn in client_ip_subnets %} +{{sn}} +{% endfor -%} +{% endif %} +{% endif %} +{% if outside_nexthop %} +gw-ip-address={{outside_nexthop}} +{% endif %} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/l2tp/chap-secrets +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +single-session=replace +{% if idle_timeout%} +lcp-echo-timeout={{idle_timeout}} +{% endif %} +lcp-echo-interval=30 + +{% if authentication['mode'] == 'radius' %} +[radius] +{% for rsrv in authentication['radiussrv']: %} +server={{rsrv}},{{authentication['radiussrv'][rsrv]['secret']}},\ +req-limit={{authentication['radiussrv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radiussrv'][rsrv]['fail-time']}} +{% endfor %} +{% if authentication['radiusopt']['timeout'] %} +timeout={{authentication['radiusopt']['timeout']}} +{% endif %} +{% if authentication['radiusopt']['acct-timeout'] %} +acct-timeout={{authentication['radiusopt']['acct-timeout']}} +{% endif %} +{% if authentication['radiusopt']['max-try'] %} +max-try={{authentication['radiusopt']['max-try']}} +{% endif %} +{% if authentication['radiusopt']['nas-id'] %} +nas-identifier={{authentication['radiusopt']['nas-id']}} +{% endif %} +{% if authentication['radiusopt']['nas-ip'] %} +nas-ip-address={{authentication['radiusopt']['nas-ip']}} +{% endif -%} +{% if authentication['radiusopt']['dae-srv'] %} +dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ +{{authentication['radiusopt']['dae-srv']['port']}},\ +{{authentication['radiusopt']['dae-srv']['secret']}} +{% endif -%} +gw-ip-address={{outside_nexthop}} +verbose=1 +{% endif -%} + +{% if authentication['radiusopt']['shaper'] %} +[shaper] +verbose=1 +attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2004 + +''' + +### l2tp chap secrets +chap_secrets_conf = ''' +# username server password acceptable local IP addresses shaper +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ +{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} +{% else %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} +{% endif %} +{% endif %} +{% endfor %} +''' + +### +# inline helper functions +### +# depending on hw and threads, daemon needs a little to start +# if it takes longer than 100 * 0.5 secs, exception is being raised +# not sure if that's the best way to check it, but it worked so far quite well +### +def chk_con(): + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", 2004)) + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt +=1 + if cnt == 100: + raise("failed to start l2tp server") + break + +### chap_secrets file if auth mode local +def write_chap_secrets(c): + tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets,'w').write(chap_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + +def accel_cmd(cmd=''): + if not cmd: + return None + try: + ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2004',cmd]).decode().strip() + return ret + except: + return 1 + +### +# inline helper functions end +### + +def get_config(): + c = Config() + if not c.exists('vpn l2tp remote-access '): + return None + + c.set_level('vpn l2tp remote-access') + config_data = { + 'authentication' : { + 'mode' : 'local', + 'local-users' : { + }, + 'radiussrv' : {}, + 'radiusopt' : {}, + 'auth_proto' : [], + 'mppe' : 'prefer' + }, + 'outside_addr' : '', + 'outside_nexthop' : '', + 'dns' : [], + 'wins' : [], + 'client_ip_pool' : None, + 'client_ip_subnets' : [], + 'mtu' : '1436', + } + + ### general options ### + + if c.exists('dns-servers server-1'): + config_data['dns'].append( c.return_value('dns-servers server-1')) + if c.exists('dns-servers server-2'): + config_data['dns'].append( c.return_value('dns-servers server-2')) + if c.exists('wins-servers server-1'): + config_data['wins'].append( c.return_value('wins-servers server-1')) + if c.exists('wins-servers server-2'): + config_data['wins'].append( c.return_value('wins-servers server-2')) + if c.exists('outside-address'): + config_data['outside_addr'] = c.return_value('outside-address') + + ### auth local + if c.exists('authentication mode local'): + if c.exists('authentication local-users username'): + for usr in c.list_nodes('authentication local-users username'): + config_data['authentication']['local-users'].update( + { + usr : { + 'passwd' : '', + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + } + ) + + if c.exists('authentication local-users username ' + usr + ' password'): + config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password') + if c.exists('authentication local-users username ' + usr + ' disable'): + config_data['authentication']['local-users'][usr]['state'] = 'disable' + if c.exists('authentication local-users username ' + usr + ' static-ip'): + config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') + if c.exists('authentication local-users username ' + usr + ' rate-limit download'): + config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download') + if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): + config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload') + + ### authentication mode radius servers and settings + + if c.exists('authentication mode radius'): + config_data['authentication']['mode'] = 'radius' + rsrvs = c.list_nodes('authentication radius server') + for rsrv in rsrvs: + if c.return_value('authentication radius server ' + rsrv + ' fail-time') == None: + ftime = '0' + else: + ftime = str(c.return_value('authentication radius server ' + rsrv + ' fail-time')) + if c.return_value('authentication radius-server ' + rsrv + ' req-limit') == None: + reql = '0' + else: + reql = str(c.return_value('authentication radius server ' + rsrv + ' req-limit')) + + config_data['authentication']['radiussrv'].update( + { + rsrv : { + 'secret' : c.return_value('authentication radius server ' + rsrv + ' key'), + 'fail-time' : ftime, + 'req-limit' : reql + } + } + ) + + #### advanced radius-setting + if c.exists('authentication radius-settings'): + if c.exists('authentication radius-settings acct-timeout'): + config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') + if c.exists('authentication radius-settings max-try'): + config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') + if c.exists('authentication radius-settings timeout'): + config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') + if c.exists('authentication radius-settings nas-identifier'): + config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') + if c.exists('authentication radius-settings nas-ip-address'): + config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') + if c.exists('authentication radius-settings dae-server'): + # Set default dae-server port if not defined + if c.exists('authentication radius-settings dae-server port'): + dae_server_port = c.return_value('authentication radius-settings dae-server port') + else: + dae_server_port = "3799" + config_data['authentication']['radiusopt'].update( + { + 'dae-srv' : { + 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), + 'port' : dae_server_port, + 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) + } + } + ) + #### filter-id is the internal accel default if attribute is empty + #### set here as default for visibility which may change in the future + if c.exists('authentication radius-settings rate-limit enable'): + if not c.exists('authentication radius-settings rate-limit attribute'): + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : 'Filter-Id' + } + else: + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : c.return_value('authentication radius-settings rate-limit attribute') + } + if c.exists('authentication radius-settings rate-limit vendor'): + config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor') + + if c.exists('client-ip-pool'): + if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'): + config_data['client_ip_pool'] = c.return_value('client-ip-pool start') + '-' + re.search('[0-9]+$', c.return_value('client-ip-pool stop')).group(0) + + if c.exists('client-ip-pool subnet'): + config_data['client_ip_subnets'] = c.return_values('client-ip-pool subnet') + + if c.exists('mtu'): + config_data['mtu'] = c.return_value('mtu') + + ### gateway address + if c.exists('outside-nexthop'): + config_data['outside_nexthop'] = c.return_value('outside-nexthop') + + if c.exists('authentication require'): + auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'} + for proto in c.return_values('authentication require'): + config_data['authentication']['auth_proto'].append(auth_mods[proto]) + else: + config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] + + if c.exists('authentication mppe'): + config_data['authentication']['mppe'] = c.return_value('authentication mppe') + + if c.exists('idle'): + config_data['idle_timeout'] = c.return_value('idle') + + return config_data + +def verify(c): + if c == None: + return None + + if c['authentication']['mode'] == 'local': + if not c['authentication']['local-users']: + raise ConfigError('l2tp-server authentication local-users required') + for usr in c['authentication']['local-users']: + if not c['authentication']['local-users'][usr]['passwd']: + raise ConfigError('user ' + usr + ' requires a password') + + if c['authentication']['mode'] == 'radius': + if len(c['authentication']['radiussrv']) == 0: + raise ConfigError('radius server required') + for rsrv in c['authentication']['radiussrv']: + if c['authentication']['radiussrv'][rsrv]['secret'] == None: + raise ConfigError('radius server ' + rsrv + ' needs a secret configured') + + ### check for the existence of a client ip pool + if not c['client_ip_pool'] and not c['client_ip_subnets']: + raise ConfigError("set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + + if not c['outside_nexthop']: + #raise ConfigError('set vpn l2tp remote-access outside-nexthop required') + print ("WARMING: set vpn l2tp remote-access outside-nexthop required") + +def generate(c): + if c == None: + return None + + ### accel-cmd reload doesn't work so any change results in a restart of the daemon + try: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + except KeyError: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + + tmpl = jinja2.Template(l2tp_config, trim_blocks=True) + config_text = tmpl.render(c) + open(l2tp_conf,'w').write(config_text) + + if c['authentication']['local-users']: + write_chap_secrets(c) + + return c + +def apply(c): + if c == None: + if os.path.exists(pidfile): + accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = subprocess.call(['/usr/sbin/accel-pppd','-c',l2tp_conf,'-p',pidfile,'-d']) + chk_con() + if ret !=0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + ### if gw ip changes, only restart doesn't work + accel_cmd('restart') + sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + +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/ipsec-settings.py b/src/conf_mode/ipsec-settings.py index 921f20491..8d25e7abd 100755 --- a/src/conf_mode/ipsec-settings.py +++ b/src/conf_mode/ipsec-settings.py @@ -16,16 +16,75 @@ # # +import sys +import re import os import jinja2 +import syslog as sl import vyos.config import vyos.defaults from vyos import ConfigError + +ra_conn_name = "remote-access" charon_conf_file = "/etc/strongswan.d/charon.conf" +ipsec_secrets_flie = "/etc/ipsec.secrets" +ipsec_ra_conn_file = "/etc/ipsec.d/tunnels/"+ra_conn_name +ipsec_conf_flie = "/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 ###" + +l2pt_ipsec_conf = ''' +{{delim_ipsec_l2tp_begin}} +include {{ipsec_ra_conn_file}} +{{delim_ipsec_l2tp_end}} +''' + +l2pt_ipsec_secrets_conf = ''' +{{delim_ipsec_l2tp_begin}} +{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} +{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" +{% elif ipsec_l2tp_auth_mode == 'x509' %} +: RSA {{server_key_file_copied}} +{% endif%} +{{delim_ipsec_l2tp_end}} +''' +l2tp_ipsec_ra_conn_conf = ''' +{{delim_ipsec_l2tp_begin}} +conn {{ra_conn_name}} + type=transport + left={{outside_addr}} + leftsubnet=%dynamic[/1701] + rightsubnet=%dynamic + mark=%unique + auto=add + ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! + dpddelay=15 + dpdtimeout=45 + dpdaction=clear + esp=aes256-sha1,3des-sha1! + rekey=no +{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} + authby=secret + leftauth=psk + rightauth=psk +{% elif ipsec_l2tp_auth_mode == 'x509' %} + authby=rsasig + leftrsasigkey=%cert + rightrsasigkey=%cert + rightca=%same + leftcert={{server_cert_file_copied}} +{% endif %} + ikelifetime={{ipsec_l2tp_ike_lifetime}} + keylife={{ipsec_l2tp_lifetime}} +{{delim_ipsec_l2tp_end}} +''' def get_config(): config = vyos.config.Config() @@ -34,10 +93,133 @@ def get_config(): 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 +### ipsec secret l2tp +def write_ipsec_secrets(c): + tmpl = jinja2.Template(l2pt_ipsec_secrets_conf, trim_blocks=True) + l2pt_ipsec_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(ipsec_secrets_flie,'w').write(l2pt_ipsec_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, ipsec_secrets_flie + ' written') + +### ipsec remote access connection config +def write_ipsec_ra_conn(c): + tmpl = jinja2.Template(l2tp_ipsec_ra_conn_conf, trim_blocks=True) + ipsec_ra_conn_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(ipsec_ra_conn_file,'w').write(ipsec_ra_conn_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, ipsec_ra_conn_file + ' written') + +### Remove config from file by delimiter +def remove_confs(delim_begin, delim_end, conf_file): + os.system("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) + + +### Append "include /path/to/ra_conn" to ipsec conf file +def append_ipsec_conf(c): + tmpl = jinja2.Template(l2pt_ipsec_conf, trim_blocks=True) + l2pt_ipsec_conf_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(ipsec_conf_flie,'a').write(l2pt_ipsec_conf_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, ipsec_conf_flie + ' written') + +### 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 = os.system('cp -f '+file_path+' '+dts_path) + if ret: + raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) + else: + sl.syslog(sl.LOG_NOTICE, file_path + ' copied to '+dts_path) + def verify(data): - pass + # 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): tmpl_path = os.path.join(vyos.defaults.directories["data"], "templates", "ipsec") @@ -51,10 +233,21 @@ def generate(data): with open(charon_conf_file, 'w') as f: f.write(charon_conf) + if data["ipsec_l2tp"]: + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) + write_ipsec_secrets(data) + write_ipsec_ra_conn(data) + append_ipsec_conf(data) + else: + 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_flie) + remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_flie) + def apply(data): # Do nothing # StrongSWAN should only be restarted when actual tunnels are configured - pass + # Restart ipsec for l2tp + os.system("ipsec restart >&/dev/null") if __name__ == '__main__': try: diff --git a/src/op_mode/show_vpn_ra.py b/src/op_mode/show_vpn_ra.py new file mode 100755 index 000000000..cf6119c2f --- /dev/null +++ b/src/op_mode/show_vpn_ra.py @@ -0,0 +1,58 @@ +#!/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 . + +import os +import sys +import re +import subprocess +# from subprocess import Popen, PIPE + +# chech connection to pptp and l2tp daemon +def get_sessions(): + absent_pptp = False + absent_l2tp = False + pptp_cmd = ["accel-cmd", "-p 2003", "show sessions"] + l2tp_cmd = ["accel-cmd", "-p 2004", "show sessions"] + err_pattern = "^Connection.+failed$" + # This value for chack only output header without sessions. + len_def_header = 170 + + # Check pptp + ret = subprocess.Popen(pptp_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (output, err) = ret.communicate() + if not err and len(output.decode("utf-8")) > len_def_header and not re.search(err_pattern, output.decode("utf-8")): + print(output.decode("utf-8")) + else: + absent_pptp = True + + # Check l2tp + ret = subprocess.Popen(l2tp_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + (output, err) = ret.communicate() + if not err and len(output.decode("utf-8")) > len_def_header and not re.search(err_pattern, output.decode("utf-8")): + print(output.decode("utf-8")) + else: + absent_l2tp = True + + if absent_l2tp and absent_pptp: + print("No active remote access VPN sessions") + + +def main(): + get_sessions() + + +if __name__ == '__main__': + main() -- cgit v1.2.3 From 3945b2259aaa64eb9f4d61334126235f2d641293 Mon Sep 17 00:00:00 2001 From: Eshenko Dmitriy Date: Thu, 25 Jul 2019 20:08:09 +0300 Subject: T1541 Fix: adding additional check --- src/conf_mode/dhcpv6_relay.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/conf_mode/dhcpv6_relay.py b/src/conf_mode/dhcpv6_relay.py index 5868abe8a..ccabc901d 100755 --- a/src/conf_mode/dhcpv6_relay.py +++ b/src/conf_mode/dhcpv6_relay.py @@ -52,9 +52,10 @@ def get_config(): if conf.exists('listen-interface'): interfaces = conf.list_nodes('listen-interface') for intf in interfaces: - addr = conf.return_value('listen-interface {0} address'.format(intf)) - listen = addr + '%' + intf - relay['listen_addr'].append(listen) + 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'): @@ -82,7 +83,7 @@ def verify(relay): 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.') + raise ConfigError('Must set at least one listen and upstream interface addresses.') return None -- cgit v1.2.3 From 8e8c0b152e6ae98bb1ad0e479c20bae0fca7279a Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Fri, 26 Jul 2019 20:32:43 +0000 Subject: T1546 fix syntax l2tp radius source-address and migrate other radius options --- interface-definitions/l2tp-server.xml | 21 ++++----- src/conf_mode/accel_l2tp.py | 80 +++++++++++++++++------------------ 2 files changed, 49 insertions(+), 52 deletions(-) diff --git a/interface-definitions/l2tp-server.xml b/interface-definitions/l2tp-server.xml index 2d103aae0..797e5a812 100644 --- a/interface-definitions/l2tp-server.xml +++ b/interface-definitions/l2tp-server.xml @@ -417,13 +417,15 @@ - - - - - RADIUS settings - - + + + Local RADIUS client address from which packets are sent. + + <x.x.x.x> + Local RADIUS client address from which packets are sent + + + Timeout to wait response from server (seconds) @@ -444,11 +446,6 @@ Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests. - - - Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address. - - IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA) diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py index 39732b97d..5f0546d63 100755 --- a/src/conf_mode/accel_l2tp.py +++ b/src/conf_mode/accel_l2tp.py @@ -141,8 +141,8 @@ max-try={{authentication['radiusopt']['max-try']}} {% if authentication['radiusopt']['nas-id'] %} nas-identifier={{authentication['radiusopt']['nas-id']}} {% endif %} -{% if authentication['radiusopt']['nas-ip'] %} -nas-ip-address={{authentication['radiusopt']['nas-ip']}} +{% if authentication['radius_source_address'] %} +nas-ip-address={{authentication['radius_source_address']}} {% endif -%} {% if authentication['radiusopt']['dae-srv'] %} dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ @@ -314,47 +314,47 @@ def get_config(): } } ) + ### Source ip address feature + if c.exists('authentication radius source-address'): + config_data['authentication']['radius_source_address'] = c.return_value('authentication radius source-address') #### advanced radius-setting - if c.exists('authentication radius-settings'): - if c.exists('authentication radius-settings acct-timeout'): - config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') - if c.exists('authentication radius-settings max-try'): - config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') - if c.exists('authentication radius-settings timeout'): - config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') - if c.exists('authentication radius-settings nas-identifier'): - config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') - if c.exists('authentication radius-settings nas-ip-address'): - config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') - if c.exists('authentication radius-settings dae-server'): - # Set default dae-server port if not defined - if c.exists('authentication radius-settings dae-server port'): - dae_server_port = c.return_value('authentication radius-settings dae-server port') - else: - dae_server_port = "3799" - config_data['authentication']['radiusopt'].update( - { - 'dae-srv' : { - 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), - 'port' : dae_server_port, - 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) - } - } - ) - #### filter-id is the internal accel default if attribute is empty - #### set here as default for visibility which may change in the future - if c.exists('authentication radius-settings rate-limit enable'): - if not c.exists('authentication radius-settings rate-limit attribute'): - config_data['authentication']['radiusopt']['shaper'] = { - 'attr' : 'Filter-Id' - } - else: - config_data['authentication']['radiusopt']['shaper'] = { - 'attr' : c.return_value('authentication radius-settings rate-limit attribute') + if c.exists('authentication radius acct-timeout'): + config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius acct-timeout') + if c.exists('authentication radius max-try'): + config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius max-try') + if c.exists('authentication radius timeout'): + config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius timeout') + if c.exists('authentication radius nas-identifier'): + config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius nas-identifier') + if c.exists('authentication radius dae-server'): + # Set default dae-server port if not defined + if c.exists('authentication radius dae-server port'): + dae_server_port = c.return_value('authentication radius dae-server port') + else: + dae_server_port = "3799" + config_data['authentication']['radiusopt'].update( + { + 'dae-srv' : { + 'ip-addr' : c.return_value('authentication radius dae-server ip-address'), + 'port' : dae_server_port, + 'secret' : str(c.return_value('authentication radius dae-server secret')) } - if c.exists('authentication radius-settings rate-limit vendor'): - config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor') + } + ) + #### filter-id is the internal accel default if attribute is empty + #### set here as default for visibility which may change in the future + if c.exists('authentication radius rate-limit enable'): + if not c.exists('authentication radius rate-limit attribute'): + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : 'Filter-Id' + } + else: + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : c.return_value('authentication radius rate-limit attribute') + } + if c.exists('authentication radius rate-limit vendor'): + config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius rate-limit vendor') if c.exists('client-ip-pool'): if c.exists('client-ip-pool start') and c.exists('client-ip-pool stop'): -- cgit v1.2.3 From a14688fb01cec6044454189eef9b74baa1d14950 Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 26 Jul 2019 16:25:57 -0700 Subject: [SSTP] - T853: accel-ppp: SSTP implementation --- interface-definitions/sstp.xml | 416 ++++++++++++++++++++++++++++++++++++ src/conf_mode/accel_sstp.py | 469 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 885 insertions(+) create mode 100644 interface-definitions/sstp.xml create mode 100755 src/conf_mode/accel_sstp.py diff --git a/interface-definitions/sstp.xml b/interface-definitions/sstp.xml new file mode 100644 index 000000000..d944baaad --- /dev/null +++ b/interface-definitions/sstp.xml @@ -0,0 +1,416 @@ + + + + + + + Secure Socket Tunneling Protocol (SSTP) Server + 900 + + + + + Authentication for remote access SSTP Server + + + + + Local user authentication for SSTP server + + + + + User name for authentication + + + + + Option to disable a SSTP Server user + + + + + + Password for authentication + + + + + Static client IP address + + + + + Upload/Download speed limits + + + + + Upload bandwidth limit in kbits/sec + + + + + + + + Download bandwidth limit in kbits/sec + + + + + + + + + + + + + + Authentication mode for SSTP Server + + local + Use local username/password configuration + + + radius + Use a RADIUS server to autenticate users + + + ^(local|radius) + + + local radius + + + + + + Authentication protocol for remote access peer SSTP VPN + + pap + Require the peer to authenticate itself using PAP [Password Authentication Protocol]. + + + chap + Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol]. + + + mschap + Require the peer to authenticate itself using CHAP [Challenge Handshake Authentication Protocol]. + + + mschap-v2 + Require the peer to authenticate itself using MS-CHAPv2 [Microsoft Challenge Handshake Authentication Protocol, Version 2]. + + + ^(pap|chap|mschap|mschap-v2) + + + pap chap mschap mschap-v2 + + + + + + + IP address of RADIUS server + + ipv4 + IP address of RADIUS server + + + + + + Key for accessing the specified server + + + + + Maximum number of simultaneous requests to server (default: unlimited) + + + + + If server doesn't responds mark it as unavailable for this amount of time in seconds + + + + + + + RADIUS settings + + + + + Timeout to wait response from server (seconds) + + + + + Timeout to wait reply for Interim-Update packets. (default 3 seconds) + + + + + Maximum number of tries to send Access-Request/Accounting-Request queries + + + + + Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests. + + + + + Value to send to RADIUS server in NAS-IP-Address attribute and to be matched in DM/CoA requests. Also DM/CoA server will bind to that address. + + + + invalid IPv4 address + + ipv4 + NAS-IP-Address Attribute Value + + + + + + IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA) + + + + + IP address for Dynamic Authorization Extension server (DM/CoA) + + + + invalid IPv4 address + + ipv4 + Specifies IP address for Dynamic Authorization Extension server (DM/CoA) + + + + + + Port for Dynamic Authorization Extension server (DM/CoA) + + number + TCP port + + + + + + + + + Secret for Dynamic Authorization Extension server (DM/CoA) + + + + + + + Upload/Download speed limits + + + + + Specifies which radius attribute contains rate information. (default is Filter-Id) + + + + + Specifies the vendor dictionary. (dictionary needs to be in /usr/share/accel-ppp/radius) + + + + + Enables Bandwidth shaping via RADIUS + + + + + + + + + + + + SSTP settings + + + + + SSL Certificate, SSL Key and CA (/config/user-data/sstp) + + + + + Certificate Authority certificate + + + + + + + + Server Certificate + + + + + + + + Privat Key of the Server Certificate + + + + + + + + + + + + Network settings + + + + + Client IP pools and gateway setting + + + + + Client IP subnet (CIDR notation) + + + + Not a valid CIDR formatted prefix + + + + + + Gateway IP address + + + + invalid IPv4 address + + ipv4 + Default Gateway send to the client + + + + + + + + DNS servers propagated to clients + + + + + Primary DNS Server + + ipv4 + IPv4 address + + + + + + + + + Secondary DNS Server + + ipv4 + IPv4 address + + + + + + + + + + + Maximum Transmission Unit (MTU) + + + + + + + + + + PPP (Point-to-Point Protocol) settings + + + + + Specifies mppe negotiation preferences + + require prefer deny + + + (^require|prefer|deny) + + + require + send mppe request, if client rejects, drop the connection + + + prefer + send mppe request, if client rejects continue + + + deny + drop all mppe + + + + + + LCP echo-requests/sec + + + + + + + + Maximum number of Echo-Requests may be sent without valid reply + + + + + + + + Timeout in seconds to wait for any peer activity. If this option specified it turns on adaptive lcp echo functionality and "lcp-echo-failure" is not used. + + + + + + + + + + + + diff --git a/src/conf_mode/accel_sstp.py b/src/conf_mode/accel_sstp.py new file mode 100755 index 000000000..1317a32db --- /dev/null +++ b/src/conf_mode/accel_sstp.py @@ -0,0 +1,469 @@ +#!/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 . +# +# + +import sys +import os +import re +import subprocess +import jinja2 +import socket +import time +import syslog as sl + +from vyos.config import Config +from vyos import ConfigError + +pidfile = r'/var/run/accel_sstp.pid' +sstp_cnf_dir = r'/etc/accel-ppp/sstp' +chap_secrets = sstp_cnf_dir + '/chap-secrets' +sstp_conf = sstp_cnf_dir + '/sstp.config' +ssl_cert_dir = r'/config/user-data/sstp' + +### config path creation +if not os.path.exists(sstp_cnf_dir): + os.makedirs(sstp_cnf_dir) + sl.syslog(sl.LOG_NOTICE, sstp_cnf_dir + " created") + +if not os.path.exists(ssl_cert_dir): + os.makedirs(ssl_cert_dir) + sl.syslog(sl.LOG_NOTICE, ssl_cert_dir + " created") + +sstp_config = ''' +### generated by accel_sstp.py ### +[modules] +log_syslog +sstp +ippool +shaper +{% if authentication['mode'] == 'local' %} +chap-secrets +{% endif -%} +{% for proto in authentication['auth_proto'] %} +{{proto}} +{% endfor %} +{% if authentication['mode'] == 'radius' %} +radius +{% endif %} + +[core] +thread-count={{thread_cnt}} + +[common] +single-session=replace + +[log] +syslog=accel-sstp,daemon +copy=1 +level=5 + +[client-ip-range] +disable + +[sstp] +verbose=1 +accept=ssl +{% if certs %} +ssl-ca-file=/config/user-data/sstp/{{certs['ca']}} +ssl-pemfile=/config/user-data/sstp/{{certs['server-cert']}} +ssl-keyfile=/config/user-data/sstp/{{certs['server-key']}} +{% endif %} + +{%if ip_pool %} +[ip-pool] +gw-ip-address={{gw}} +{% for sn in ip_pool %} +{{sn}} +{% endfor %} +{% endif %} + +{% if dnsv4 %} +[dns] +{% if dnsv4['primary'] %} +dns1={{dnsv4['primary']}} +{% endif -%} +{% if dnsv4['secondary'] %} +dns2={{dnsv4['secondary']}} +{% endif -%} +{% endif %} + +{% if authentication['mode'] == 'local' %} +[chap-secrets] +chap-secrets=/etc/accel-ppp/sstp/chap-secrets +{% endif %} + +{%- if authentication['mode'] == 'radius' %} +[radius] +verbose=1 +{% for rsrv in authentication['radius-srv']: %} +server={{rsrv}},{{authentication['radius-srv'][rsrv]['secret']}},\ +req-limit={{authentication['radius-srv'][rsrv]['req-limit']}},\ +fail-time={{authentication['radius-srv'][rsrv]['fail-time']}} +{% endfor -%} +{% if authentication['radiusopt']['acct-timeout'] %} +acct-timeout={{authentication['radiusopt']['acct-timeout']}} +{% endif -%} +{% if authentication['radiusopt']['timeout'] %} +timeout={{authentication['radiusopt']['timeout']}} +{% endif -%} +{% if authentication['radiusopt']['max-try'] %} +max-try={{authentication['radiusopt']['max-try']}} +{% endif -%} +{% if authentication['radiusopt']['nas-id'] %} +nas-identifier={{authentication['radiusopt']['nas-id']}} +{% endif -%} +{% if authentication['radiusopt']['nas-ip'] %} +nas-ip-address={{authentication['radiusopt']['nas-ip']}} +{% endif -%} +{% if authentication['radiusopt']['dae-srv'] %} +dae-server={{authentication['radiusopt']['dae-srv']['ip-addr']}}:\ +{{authentication['radiusopt']['dae-srv']['port']}},\ +{{authentication['radiusopt']['dae-srv']['secret']}} +{% endif -%} +{% endif %} + +[ppp] +verbose=1 +check-ip=1 +{% if mtu %} +mtu={{mtu}} +{% endif -%} +{% if ppp['mppe'] %} +mppe={{ppp['mppe']}} +{% endif -%} +{% if ppp['lcp-echo-interval'] %} +lcp-echo-interval={{ppp['lcp-echo-interval']}} +{% endif -%} +{% if ppp['lcp-echo-failure'] %} +lcp-echo-failure={{ppp['lcp-echo-failure']}} +{% endif -%} +{% if ppp['lcp-echo-timeout'] %} +lcp-echo-timeout={{ppp['lcp-echo-timeout']}} +{% endif %} + +{% if authentication['radiusopt']['shaper'] %} +[shaper] +verbose=1 +attr={{authentication['radiusopt']['shaper']['attr']}} +{% if authentication['radiusopt']['shaper']['vendor'] %} +vendor={{authentication['radiusopt']['shaper']['vendor']}} +{% endif -%} +{% endif %} + +[cli] +tcp=127.0.0.1:2005 +''' + +### sstp chap secrets +chap_secrets_conf = ''' +# username server password acceptable local IP addresses shaper +{% for user in authentication['local-users'] %} +{% if authentication['local-users'][user]['state'] == 'enabled' %} +{% if (authentication['local-users'][user]['upload']) and (authentication['local-users'][user]['download']) %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}}\t\ +{{authentication['local-users'][user]['download']}}/{{authentication['local-users'][user]['upload']}} +{% else %} +{{user}}\t*\t{{authentication['local-users'][user]['passwd']}}\t{{authentication['local-users'][user]['ip']}} +{% endif %} +{% endif %} +{% endfor %} +''' +### +# inline helper functions +### +# depending on hw and threads, daemon needs a little to start +# if it takes longer than 100 * 0.5 secs, exception is being raised +# not sure if that's the best way to check it, but it worked so far quite well +### +def chk_con(): + cnt = 0 + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + while True: + try: + s.connect(("127.0.0.1", 2005)) + s.close() + break + except ConnectionRefusedError: + time.sleep(0.5) + cnt +=1 + if cnt == 100: + raise("failed to start sstp server") + break + +### chap_secrets file if auth mode local +def write_chap_secrets(c): + tmpl = jinja2.Template(chap_secrets_conf, trim_blocks=True) + chap_secrets_txt = tmpl.render(c) + old_umask = os.umask(0o077) + open(chap_secrets,'w').write(chap_secrets_txt) + os.umask(old_umask) + sl.syslog(sl.LOG_NOTICE, chap_secrets + ' written') + +def accel_cmd(cmd=''): + if not cmd: + return None + try: + ret = subprocess.check_output(['/usr/bin/accel-cmd','-p','2005',cmd]).decode().strip() + return ret + except: + return 1 + +#### check ig local-ip is in client pool subnet + + +### +# inline helper functions end +### + +def get_config(): + c = Config() + if not c.exists('service sstp-server'): + return None + + c.set_level('service sstp-server') + + config_data = { + 'authentication' : { + 'local-users' : { + }, + 'mode' : 'local', + 'auth_proto' : [], + 'radius-srv' : {}, + 'radiusopt' : {}, + 'dae-srv' : {} + }, + 'certs' : { + 'ca' : None, + 'server-key' : None, + 'server-cert' : None + }, + 'ip_pool' : [], + 'gw' : None, + 'dnsv4' : {}, + 'mtu' : None, + 'ppp' : {}, + } + + ### local auth + if c.exists('authentication mode local'): + if c.exists('authentication local-users'): + for usr in c.list_nodes('authentication local-users username'): + config_data['authentication']['local-users'].update( + { + usr : { + 'passwd' : None, + 'state' : 'enabled', + 'ip' : '*', + 'upload' : None, + 'download' : None + } + } + ) + if c.exists('authentication local-users username ' + usr + ' password'): + config_data['authentication']['local-users'][usr]['passwd'] = c.return_value('authentication local-users username ' + usr + ' password') + if c.exists('authentication local-users username ' + usr + ' disable'): + config_data['authentication']['local-users'][usr]['state'] = 'disable' + if c.exists('authentication local-users username ' + usr + ' static-ip'): + config_data['authentication']['local-users'][usr]['ip'] = c.return_value('authentication local-users username ' + usr + ' static-ip') + if c.exists('authentication local-users username ' + usr + ' rate-limit download'): + config_data['authentication']['local-users'][usr]['download'] = c.return_value('authentication local-users username ' + usr + ' rate-limit download') + if c.exists('authentication local-users username ' + usr + ' rate-limit upload'): + config_data['authentication']['local-users'][usr]['upload'] = c.return_value('authentication local-users username ' + usr + ' rate-limit upload') + + if c.exists('authentication protocols'): + auth_mods = {'pap' : 'pap','chap' : 'auth_chap_md5', 'mschap' : 'auth_mschap_v1', 'mschap-v2' : 'auth_mschap_v2'} + for proto in c.return_values('authentication protocols'): + config_data['authentication']['auth_proto'].append(auth_mods[proto]) + else: + config_data['authentication']['auth_proto'] = ['auth_mschap_v2'] + + #### RADIUS auth and settings + if c.exists('authentication mode radius'): + config_data['authentication']['mode'] = c.return_value('authentication mode') + if c.exists('authentication radius-server'): + for rsrv in c.list_nodes('authentication radius-server'): + config_data['authentication']['radius-srv'][rsrv] = {} + if c.exists('authentication radius-server ' + rsrv + ' secret'): + config_data['authentication']['radius-srv'][rsrv]['secret'] = c.return_value('authentication radius-server ' + rsrv + ' secret') + else: + config_data['authentication']['radius-srv'][rsrv]['secret'] = None + if c.exists('authentication radius-server ' + rsrv + ' fail-time'): + config_data['authentication']['radius-srv'][rsrv]['fail-time'] = c.return_value('authentication radius-server ' + rsrv + ' fail-time') + else: + config_data['authentication']['radius-srv'][rsrv]['fail-time'] = 0 + if c.exists('authentication radius-server ' + rsrv + ' req-limit'): + config_data['authentication']['radius-srv'][rsrv]['req-limit'] = c.return_value('authentication radius-server ' + rsrv + ' req-limit') + else: + config_data['authentication']['radius-srv'][rsrv]['req-limit'] = 0 + + #### advanced radius-setting + if c.exists('authentication radius-settings'): + if c.exists('authentication radius-settings acct-timeout'): + config_data['authentication']['radiusopt']['acct-timeout'] = c.return_value('authentication radius-settings acct-timeout') + if c.exists('authentication radius-settings max-try'): + config_data['authentication']['radiusopt']['max-try'] = c.return_value('authentication radius-settings max-try') + if c.exists('authentication radius-settings timeout'): + config_data['authentication']['radiusopt']['timeout'] = c.return_value('authentication radius-settings timeout') + if c.exists('authentication radius-settings nas-identifier'): + config_data['authentication']['radiusopt']['nas-id'] = c.return_value('authentication radius-settings nas-identifier') + if c.exists('authentication radius-settings nas-ip-address'): + config_data['authentication']['radiusopt']['nas-ip'] = c.return_value('authentication radius-settings nas-ip-address') + if c.exists('authentication radius-settings dae-server'): + config_data['authentication']['radiusopt'].update( + { + 'dae-srv' : { + 'ip-addr' : c.return_value('authentication radius-settings dae-server ip-address'), + 'port' : c.return_value('authentication radius-settings dae-server port'), + 'secret' : str(c.return_value('authentication radius-settings dae-server secret')) + } + } + ) + if c.exists('authentication radius-settings rate-limit enable'): + if not c.exists('authentication radius-settings rate-limit attribute'): + config_data['authentication']['radiusopt']['shaper'] = { 'attr' : 'Filter-Id' } + else: + config_data['authentication']['radiusopt']['shaper'] = { + 'attr' : c.return_value('authentication radius-settings rate-limit attribute') + } + if c.exists('authentication radius-settings rate-limit vendor'): + config_data['authentication']['radiusopt']['shaper']['vendor'] = c.return_value('authentication radius-settings rate-limit vendor') + + if c.exists('sstp-settings ssl-certs ca'): + config_data['certs']['ca'] = c.return_value('sstp-settings ssl-certs ca') + if c.exists('sstp-settings ssl-certs server-cert'): + config_data['certs']['server-cert'] = c.return_value('sstp-settings ssl-certs server-cert') + if c.exists('sstp-settings ssl-certs server-key'): + config_data['certs']['server-key'] = c.return_value('sstp-settings ssl-certs server-key') + + if c.exists('network-settings client-ip-settings subnet'): + config_data['ip_pool'] = c.return_values('network-settings client-ip-settings subnet') + if c.exists('network-settings client-ip-settings gateway-address'): + config_data['gw'] = c.return_value('network-settings client-ip-settings gateway-address') + if c.exists('network-settings dns-server primary-dns'): + config_data['dnsv4']['primary'] = c.return_value('network-settings dns-server primary-dns') + if c.exists('network-settings dns-server secondary-dns'): + config_data['dnsv4']['secondary'] = c.return_value('network-settings dns-server secondary-dns') + if c.exists('network-settings mtu'): + config_data['mtu'] = c.return_value('network-settings mtu') + + #### ppp + if c.exists('ppp-settings mppe'): + config_data['ppp']['mppe'] = c.return_value('ppp-settings mppe') + if c.exists('ppp-settings lcp-echo-failure'): + config_data['ppp']['lcp-echo-failure'] = c.return_value('ppp-settings lcp-echo-failure') + if c.exists('ppp-settings lcp-echo-interval'): + config_data['ppp']['lcp-echo-interval'] = c.return_value('ppp-settings lcp-echo-interval') + if c.exists('ppp-settings lcp-echo-timeout'): + config_data['ppp']['lcp-echo-timeout'] = c.return_value('ppp-settings lcp-echo-timeout') + + return config_data + +def verify(c): + if c == None: + return None + ### vertify auth settings + if c['authentication']['mode'] == 'local': + if not c['authentication']['local-users']: + raise ConfigError('sstp-server authentication local-users required') + + for usr in c['authentication']['local-users']: + if not c['authentication']['local-users'][usr]['passwd']: + raise ConfigError('user ' + usr + ' requires a password') + ### if up/download is set, check that both have a value + if c['authentication']['local-users'][usr]['upload']: + if not c['authentication']['local-users'][usr]['download']: + raise ConfigError('user ' + usr + ' requires download speed value') + if c['authentication']['local-users'][usr]['download']: + if not c['authentication']['local-users'][usr]['upload']: + raise ConfigError('user ' + usr + ' requires upload speed value') + + if not c['certs']['ca'] or not c['certs']['server-key'] or not c['certs']['server-cert']: + raise ConfigError('service sstp-server sstp-settings ssl-certs needs the ssl certificates set up') + else: + ssl_path = ssl_cert_dir + '/' + if not os.path.exists(ssl_path + c['certs']['ca']): + raise ConfigError('CA {0} doesn\'t exist'.format(ssl_path + c['certs']['ca'])) + if not os.path.exists(ssl_path + c['certs']['server-cert']): + raise ConfigError('SSL Cert {0} doesn\'t exist'.format(ssl_path + c['certs']['server-cert'])) + if not os.path.exists(ssl_path + c['certs']['server-cert']): + raise ConfigError('SSL Key {0} doesn\'t exist'.format(ssl_path + c['certs']['server-key'])) + + if c['authentication']['mode'] == 'radius': + if len(c['authentication']['radius-srv']) == 0: + raise ConfigError('service sstp-server authentication radius-server needs a value') + for rsrv in c['authentication']['radius-srv']: + if c['authentication']['radius-srv'][rsrv]['secret'] == None: + raise ConfigError('service sstp-server authentication radius-server {0} secret requires a value'.format(rsrv)) + + if c['authentication']['mode'] == 'local': + if not c['ip_pool']: + print ("WARNING: service sstp-server network-settings client-ip-settings subnet requires a value") + if not c['gw']: + print ("WARNING: service sstp-server network-settings client-ip-settings gateway-address requires a value") + +def generate(c): + if c == None: + return None + + ### accel-cmd reload doesn't work so any change results in a restart of the daemon + try: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + except KeyError: + if os.cpu_count() == 1: + c['thread_cnt'] = 1 + else: + c['thread_cnt'] = int(os.cpu_count()/2) + + tmpl = jinja2.Template(sstp_config, trim_blocks=True) + config_text = tmpl.render(c) + open(sstp_conf,'w').write(config_text) + + if c['authentication']['local-users']: + write_chap_secrets(c) + + return c + +def apply(c): + if c == None: + if os.path.exists(pidfile): + accel_cmd('shutdown hard') + if os.path.exists(pidfile): + os.remove(pidfile) + return None + + if not os.path.exists(pidfile): + ret = subprocess.call(['/usr/sbin/accel-pppd','-c',sstp_conf,'-p',pidfile,'-d']) + chk_con() + if ret !=0 and os.path.exists(pidfile): + os.remove(pidfile) + raise ConfigError('accel-pppd failed to start') + else: + accel_cmd('restart') + sl.syslog(sl.LOG_NOTICE, "reloading config via daemon restart") + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From 93767c0fe4b87327b18ddd4aecb540596d38aaf1 Mon Sep 17 00:00:00 2001 From: hagbard Date: Wed, 31 Jul 2019 09:56:48 -0700 Subject: [SSTP] - T853: adding show commands for session and stats --- op-mode-definitions/sstp-server.xml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 op-mode-definitions/sstp-server.xml diff --git a/op-mode-definitions/sstp-server.xml b/op-mode-definitions/sstp-server.xml new file mode 100644 index 000000000..36d0b9985 --- /dev/null +++ b/op-mode-definitions/sstp-server.xml @@ -0,0 +1,26 @@ + + + + + + + show sstp-server status + + + + + Show active SSTP server sessions + + /usr/bin/accel-cmd -p 2005 'show sessions ifname,username,ip,ip6,ip6-dp,calling-sid,rate-limit,state,uptime,rx-bytes,tx-bytes' + + + + Show SSTP server statistics + + /usr/bin/accel-cmd -p 2005 'show stat' + + + + + + -- cgit v1.2.3 From 6781c4f1f38aed2bbf69ef4d3a8c92eea3946b17 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Wed, 31 Jul 2019 23:00:08 +0000 Subject: T1555 Implementation shared-secret for LNS. Implementation command disabling ccp. --- interface-definitions/l2tp-server.xml | 18 ++++++++++++++++++ src/conf_mode/accel_l2tp.py | 15 ++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/interface-definitions/l2tp-server.xml b/interface-definitions/l2tp-server.xml index 797e5a812..d5b6a921b 100644 --- a/interface-definitions/l2tp-server.xml +++ b/interface-definitions/l2tp-server.xml @@ -67,6 +67,24 @@ + + + L2TP Network Server (LNS) + + + + + Tunnel password used to authenticate the client (LAC) + + + + + + + Disable Compression Control Protocol (CCP) + + + Internet Protocol Security (IPsec) for remote access L2TP VPN diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py index 5f0546d63..3a224974e 100755 --- a/src/conf_mode/accel_l2tp.py +++ b/src/conf_mode/accel_l2tp.py @@ -89,6 +89,9 @@ mppe={{authentication['mppe']}} {% if outside_addr %} bind={{outside_addr}} {% endif %} +{% if lns_shared_secret %} +secret={{lns_shared_secret}} +{% endif %} [client-ip-range] 0.0.0.0/0 @@ -117,10 +120,13 @@ chap-secrets=/etc/accel-ppp/l2tp/chap-secrets verbose=1 check-ip=1 single-session=replace -{% if idle_timeout%} +{% if idle_timeout %} lcp-echo-timeout={{idle_timeout}} {% endif %} lcp-echo-interval=30 +{% if ccp_disable %} +ccp=0 +{% endif %} {% if authentication['mode'] == 'radius' %} [radius] @@ -383,6 +389,13 @@ def get_config(): if c.exists('idle'): config_data['idle_timeout'] = c.return_value('idle') + ### LNS secret + if c.exists('lns shared-secret'): + config_data['lns_shared_secret'] = c.return_value('lns shared-secret') + + if c.exists('ccp-disable'): + config_data['ccp_disable'] = True + return config_data def verify(c): -- cgit v1.2.3 From b5c1b646beb025bce40cf1a5fb647ab39070da58 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Aug 2019 11:34:56 +0200 Subject: WireGuard: rename wireguard.xml -> interfaces-wireguard.xml --- interface-definitions/interfaces-wireguard.xml | 141 +++++++++++++++++++++++++ interface-definitions/wireguard.xml | 141 ------------------------- 2 files changed, 141 insertions(+), 141 deletions(-) create mode 100644 interface-definitions/interfaces-wireguard.xml delete mode 100644 interface-definitions/wireguard.xml diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml new file mode 100644 index 000000000..9cfcd32ee --- /dev/null +++ b/interface-definitions/interfaces-wireguard.xml @@ -0,0 +1,141 @@ + + + + + + + WireGuard interface name + 459 + + ^wg[0-9]{1,4} + + illegal interface name + + wgN + WireGuard interface name + + + + + + IP address + + + + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + + + + + description + + ^.{1,100}$ + + interface description is too long (limit 100 characters) + + + + + disables interface + + + + + + Local port number to accept connections + + + + + + + + interface mtu size(default: 1420) + + + + + + + + A 32-bit fwmark value set on all outgoing packets + + number + value which marks the packet for QoS/shaper + + + + + + + + + peer alias + + [^ ]{1,100}$ + + peer alias too long (limit 100 characters) + + + + + disables peer + + + + + + base64 encoded public key + + ^[0-9a-zA-Z\+/]{43}=$ + + Key is not valid 44-character (32-bytes) base64 + + + + + base64 encoded preshared key + + ^[0-9a-zA-Z\+/]{43}=$ + + Key is not valid 44-character (32-bytes) base64 + + + + + IP addresses allowed to traverse the peer + + + + + + + + + + Remote endpoint (IP:port) + + + + + how often send keep alives in seconds + + + + + + + + + + + + diff --git a/interface-definitions/wireguard.xml b/interface-definitions/wireguard.xml deleted file mode 100644 index 9cfcd32ee..000000000 --- a/interface-definitions/wireguard.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - WireGuard interface name - 459 - - ^wg[0-9]{1,4} - - illegal interface name - - wgN - WireGuard interface name - - - - - - IP address - - - - - ipv4net - IPv4 address and prefix length - - - ipv6net - IPv6 address and prefix length - - - - - - - description - - ^.{1,100}$ - - interface description is too long (limit 100 characters) - - - - - disables interface - - - - - - Local port number to accept connections - - - - - - - - interface mtu size(default: 1420) - - - - - - - - A 32-bit fwmark value set on all outgoing packets - - number - value which marks the packet for QoS/shaper - - - - - - - - - peer alias - - [^ ]{1,100}$ - - peer alias too long (limit 100 characters) - - - - - disables peer - - - - - - base64 encoded public key - - ^[0-9a-zA-Z\+/]{43}=$ - - Key is not valid 44-character (32-bytes) base64 - - - - - base64 encoded preshared key - - ^[0-9a-zA-Z\+/]{43}=$ - - Key is not valid 44-character (32-bytes) base64 - - - - - IP addresses allowed to traverse the peer - - - - - - - - - - Remote endpoint (IP:port) - - - - - how often send keep alives in seconds - - - - - - - - - - - - -- cgit v1.2.3 From 8312824b556efd5cc9232a9965fb26548884fe8f Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Aug 2019 11:37:58 +0200 Subject: WireGuard: rename wireguard.py -> interface-wireguard.py --- interface-definitions/interfaces-wireguard.xml | 2 +- src/conf_mode/interface-wireguard.py | 373 +++++++++++++++++++++++++ src/conf_mode/wireguard.py | 373 ------------------------- 3 files changed, 374 insertions(+), 374 deletions(-) create mode 100755 src/conf_mode/interface-wireguard.py delete mode 100755 src/conf_mode/wireguard.py diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index 9cfcd32ee..c0102ea54 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -2,7 +2,7 @@ - + WireGuard interface name 459 diff --git a/src/conf_mode/interface-wireguard.py b/src/conf_mode/interface-wireguard.py new file mode 100755 index 000000000..8234fad0b --- /dev/null +++ b/src/conf_mode/interface-wireguard.py @@ -0,0 +1,373 @@ +#!/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 . +# +# + +import sys +import os +import re +import syslog as sl +import subprocess + +from vyos.config import Config +from vyos import ConfigError + +dir = r'/config/auth/wireguard' +pk = dir + '/private.key' +pub = dir + '/public.key' +psk_file = r'/tmp/psk' + +def check_kmod(): + if not os.path.exists('/sys/module/wireguard'): + sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") + if os.system('sudo modprobe wireguard') != 0: + sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") + raise ConfigError("modprobe wireguard failed") + +def get_config(): + c = Config() + if not c.exists('interfaces wireguard'): + return None + + c.set_level('interfaces') + intfcs = c.list_nodes('wireguard') + intfcs_eff = c.list_effective_nodes('wireguard') + new_lst = list(set(intfcs) - set(intfcs_eff)) + del_lst = list(set(intfcs_eff) - set(intfcs)) + + config_data = { + 'interfaces' : {} + } + ### setting defaults and determine status of the config + for intfc in intfcs: + cnf = 'wireguard ' + intfc + # default data struct + config_data['interfaces'].update( + { + intfc : { + 'addr' : '', + 'descr' : intfc, ## snmp ifAlias + 'lport' : '', + 'status' : 'exists', + 'state' : 'enabled', + 'fwmark' : 0x00, + 'mtu' : '1420', + 'peer' : {} + } + } + ) + + ### determine status either delete or create + for i in new_lst: + config_data['interfaces'][i]['status'] = 'create' + + for i in del_lst: + config_data['interfaces'].update( + { + i : { + 'status': 'delete' + } + } + ) + + ### based on the status, setup conf values + for intfc in intfcs: + cnf = 'wireguard ' + intfc + if config_data['interfaces'][intfc]['status'] != 'delete': + ### addresses + if c.exists(cnf + ' address'): + config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') + ### interface up/down + if c.exists(cnf + ' disable'): + config_data['interfaces'][intfc]['state'] = 'disable' + ### listen port + if c.exists(cnf + ' port'): + config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') + ### fwmark + if c.exists(cnf + ' fwmark'): + config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') + ### description + if c.exists(cnf + ' description'): + config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') + ### mtu + if c.exists(cnf + ' mtu'): + config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu') + ### peers + if c.exists(cnf + ' peer'): + for p in c.list_nodes(cnf + ' peer'): + if not c.exists(cnf + ' peer ' + p + ' disable'): + config_data['interfaces'][intfc]['peer'].update( + { + p : { + 'allowed-ips' : [], + 'endpoint' : '', + 'pubkey' : '' + } + } + ) + if c.exists(cnf + ' peer ' + p + ' pubkey'): + config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey') + if c.exists(cnf + ' peer ' + p + ' allowed-ips'): + config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips') + if c.exists(cnf + ' peer ' + p + ' endpoint'): + config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint') + if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): + config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') + if c.exists(cnf + ' peer ' + p + ' preshared-key'): + config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key') + + return config_data + +def verify(c): + if not c: + return None + + for i in c['interfaces']: + if c['interfaces'][i]['status'] != 'delete': + if not c['interfaces'][i]['addr']: + raise ConfigError("address required for interface " + i) + if not c['interfaces'][i]['peer']: + raise ConfigError("peer required on interface " + i) + + for p in c['interfaces'][i]['peer']: + if not c['interfaces'][i]['peer'][p]['allowed-ips']: + raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) + if not c['interfaces'][i]['peer'][p]['pubkey']: + raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p) + + +def apply(c): + ### no wg config left, delete all wireguard devices on the os + if not c: + net_devs = os.listdir('/sys/class/net/') + for dev in net_devs: + if os.path.isdir('/sys/class/net/' + dev): + buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() + if re.search("DEVTYPE=wireguard", buf, re.I|re.M): + wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0)) + sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) + subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) + return None + + ### + ## find the diffs between effective config an new config + ### + c_eff = Config() + c_eff.set_level('interfaces wireguard') + + ### link status up/down aka interface disable + + for intf in c['interfaces']: + if not c['interfaces'][intf]['status'] == 'delete': + if c['interfaces'][intf]['state'] == 'disable': + sl.syslog(sl.LOG_NOTICE, "disable interface " + intf) + subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True) + else: + sl.syslog(sl.LOG_NOTICE, "enable interface " + intf) + subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True) + + ### deletion of a specific interface + for intf in c['interfaces']: + if c['interfaces'][intf]['status'] == 'delete': + sl.syslog(sl.LOG_NOTICE, "removing interface " + intf) + subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True) + + ### peer deletion + peer_eff = c_eff.list_effective_nodes( intf + ' peer') + peer_cnf = [] + try: + for p in c['interfaces'][intf]['peer']: + peer_cnf.append(p) + except KeyError: + pass + + peer_rem = list(set(peer_eff) - set(peer_cnf)) + for p in peer_rem: + pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') + remove_peer(intf, pkey) + + ### peer pubkey update + ### wg identifies peers by its pubky, so we have to remove the peer first + ### it will recreated it then below with the new key from the cli config + for p in peer_eff: + if p in peer_cnf: + ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') + nkey = c['interfaces'][intf]['peer'][p]['pubkey'] + if nkey != ekey: + sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf) + remove_peer(intf, ekey) + + ### new config + if c['interfaces'][intf]['status'] == 'create': + if not os.path.exists(pk): + raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") + + subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True) + for addr in c['interfaces'][intf]['addr']: + add_addr(intf, addr) + + subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True) + configure_interface(c, intf) + + ### config updates + if c['interfaces'][intf]['status'] == 'exists': + ### IP address change + addr_eff = c_eff.return_effective_values(intf + ' address') + addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) + addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) + + if len(addr_rem) != 0: + for addr in addr_rem: + del_addr(intf, addr) + + if len(addr_add) != 0: + for addr in addr_add: + add_addr(intf, addr) + + ## mtu update + mtu = c['interfaces'][intf]['mtu'] + if mtu != 1420: + sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf) + subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True) + + + ### persistent-keepalive + for p in c['interfaces'][intf]['peer']: + val_eff = "" + val = "" + + try: + val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + except KeyError: + pass + + if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): + val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') + + ### disable keepalive + if val_eff and not val: + c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 + + ### set new keepalive value + if not val_eff and val: + c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val + + ## wg command call + configure_interface(c, intf) + + ### ifalias for snmp from description + if c['interfaces'][intf]['status'] != 'delete': + descr_eff = c_eff.return_effective_value(intf + ' description') + cnf_descr = c['interfaces'][intf]['descr'] + if descr_eff != cnf_descr: + with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh: + fh.write(str(cnf_descr)) + +def configure_interface(c, intf): + for p in c['interfaces'][intf]['peer']: + ## config init for wg call + wg_config = { + 'interface' : intf, + 'port' : 0, + 'private-key' : pk, + 'pubkey' : '', + 'psk' : '/dev/null', + 'allowed-ips' : [], + 'fwmark' : 0x00, + 'endpoint' : None, + 'keepalive' : 0 + } + + ## mandatory settings + wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey'] + wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips'] + + ## optional settings + # listen-port + if c['interfaces'][intf]['lport']: + wg_config['port'] = c['interfaces'][intf]['lport'] + + ## fwmark + if c['interfaces'][intf]['fwmark']: + wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] + + ## endpoint + if c['interfaces'][intf]['peer'][p]['endpoint']: + wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] + + ## persistent-keepalive + if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: + wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] + + ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either + if 'psk' in c['interfaces'][intf]['peer'][p]: + old_umask = os.umask(0o077) + open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk'])) + os.umask(old_umask) + wg_config['psk'] = psk_file + + ### assemble wg command + cmd = "sudo wg set " + intf + cmd += " listen-port " + str(wg_config['port']) + cmd += " fwmark " + str(wg_config['fwmark']) + cmd += " private-key " + wg_config['private-key'] + cmd += " peer " + wg_config['pubkey'] + cmd += " preshared-key " + wg_config['psk'] + cmd += " allowed-ips " + for ap in wg_config['allowed-ips']: + if ap != wg_config['allowed-ips'][-1]: + cmd += ap + "," + else: + cmd += ap + + if wg_config['endpoint']: + cmd += " endpoint " + wg_config['endpoint'] + + if wg_config['keepalive'] != 0: + cmd += " persistent-keepalive " + wg_config['keepalive'] + else: + cmd += " persistent-keepalive 0" + + sl.syslog(sl.LOG_NOTICE, cmd) + #print (cmd) + subprocess.call([cmd], shell=True) + """ remove psk_file """ + if os.path.exists(psk_file): + os.remove(psk_file) + +def add_addr(intf, addr): + # see https://phabricator.vyos.net/T949 + ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) + sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr) + +def del_addr(intf, addr): + ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) + sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) + +def remove_peer(intf, peer_key): + cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null' + ret = subprocess.call([cmd], shell=True) + sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf) + +if __name__ == '__main__': + try: + check_kmod() + c = get_config() + verify(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) diff --git a/src/conf_mode/wireguard.py b/src/conf_mode/wireguard.py deleted file mode 100755 index 8234fad0b..000000000 --- a/src/conf_mode/wireguard.py +++ /dev/null @@ -1,373 +0,0 @@ -#!/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 . -# -# - -import sys -import os -import re -import syslog as sl -import subprocess - -from vyos.config import Config -from vyos import ConfigError - -dir = r'/config/auth/wireguard' -pk = dir + '/private.key' -pub = dir + '/public.key' -psk_file = r'/tmp/psk' - -def check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - sl.syslog(sl.LOG_NOTICE, "loading wirguard kmod") - if os.system('sudo modprobe wireguard') != 0: - sl.syslog(sl.LOG_NOTICE, "modprobe wireguard failed") - raise ConfigError("modprobe wireguard failed") - -def get_config(): - c = Config() - if not c.exists('interfaces wireguard'): - return None - - c.set_level('interfaces') - intfcs = c.list_nodes('wireguard') - intfcs_eff = c.list_effective_nodes('wireguard') - new_lst = list(set(intfcs) - set(intfcs_eff)) - del_lst = list(set(intfcs_eff) - set(intfcs)) - - config_data = { - 'interfaces' : {} - } - ### setting defaults and determine status of the config - for intfc in intfcs: - cnf = 'wireguard ' + intfc - # default data struct - config_data['interfaces'].update( - { - intfc : { - 'addr' : '', - 'descr' : intfc, ## snmp ifAlias - 'lport' : '', - 'status' : 'exists', - 'state' : 'enabled', - 'fwmark' : 0x00, - 'mtu' : '1420', - 'peer' : {} - } - } - ) - - ### determine status either delete or create - for i in new_lst: - config_data['interfaces'][i]['status'] = 'create' - - for i in del_lst: - config_data['interfaces'].update( - { - i : { - 'status': 'delete' - } - } - ) - - ### based on the status, setup conf values - for intfc in intfcs: - cnf = 'wireguard ' + intfc - if config_data['interfaces'][intfc]['status'] != 'delete': - ### addresses - if c.exists(cnf + ' address'): - config_data['interfaces'][intfc]['addr'] = c.return_values(cnf + ' address') - ### interface up/down - if c.exists(cnf + ' disable'): - config_data['interfaces'][intfc]['state'] = 'disable' - ### listen port - if c.exists(cnf + ' port'): - config_data['interfaces'][intfc]['lport'] = c.return_value(cnf + ' port') - ### fwmark - if c.exists(cnf + ' fwmark'): - config_data['interfaces'][intfc]['fwmark'] = c.return_value(cnf + ' fwmark') - ### description - if c.exists(cnf + ' description'): - config_data['interfaces'][intfc]['descr'] = c.return_value(cnf + ' description') - ### mtu - if c.exists(cnf + ' mtu'): - config_data['interfaces'][intfc]['mtu'] = c.return_value(cnf + ' mtu') - ### peers - if c.exists(cnf + ' peer'): - for p in c.list_nodes(cnf + ' peer'): - if not c.exists(cnf + ' peer ' + p + ' disable'): - config_data['interfaces'][intfc]['peer'].update( - { - p : { - 'allowed-ips' : [], - 'endpoint' : '', - 'pubkey' : '' - } - } - ) - if c.exists(cnf + ' peer ' + p + ' pubkey'): - config_data['interfaces'][intfc]['peer'][p]['pubkey'] = c.return_value(cnf + ' peer ' + p + ' pubkey') - if c.exists(cnf + ' peer ' + p + ' allowed-ips'): - config_data['interfaces'][intfc]['peer'][p]['allowed-ips'] = c.return_values(cnf + ' peer ' + p + ' allowed-ips') - if c.exists(cnf + ' peer ' + p + ' endpoint'): - config_data['interfaces'][intfc]['peer'][p]['endpoint'] = c.return_value(cnf + ' peer ' + p + ' endpoint') - if c.exists(cnf + ' peer ' + p + ' persistent-keepalive'): - config_data['interfaces'][intfc]['peer'][p]['persistent-keepalive'] = c.return_value(cnf + ' peer ' + p + ' persistent-keepalive') - if c.exists(cnf + ' peer ' + p + ' preshared-key'): - config_data['interfaces'][intfc]['peer'][p]['psk'] = c.return_value(cnf + ' peer ' + p + ' preshared-key') - - return config_data - -def verify(c): - if not c: - return None - - for i in c['interfaces']: - if c['interfaces'][i]['status'] != 'delete': - if not c['interfaces'][i]['addr']: - raise ConfigError("address required for interface " + i) - if not c['interfaces'][i]['peer']: - raise ConfigError("peer required on interface " + i) - - for p in c['interfaces'][i]['peer']: - if not c['interfaces'][i]['peer'][p]['allowed-ips']: - raise ConfigError("allowed-ips required on interface " + i + " for peer " + p) - if not c['interfaces'][i]['peer'][p]['pubkey']: - raise ConfigError("pubkey from your peer is mandatory on " + i + " for peer " + p) - - -def apply(c): - ### no wg config left, delete all wireguard devices on the os - if not c: - net_devs = os.listdir('/sys/class/net/') - for dev in net_devs: - if os.path.isdir('/sys/class/net/' + dev): - buf = open('/sys/class/net/' + dev + '/uevent', 'r').read() - if re.search("DEVTYPE=wireguard", buf, re.I|re.M): - wg_intf = re.sub("INTERFACE=", "", re.search("INTERFACE=.*", buf, re.I|re.M).group(0)) - sl.syslog(sl.LOG_NOTICE, "removing interface " + wg_intf) - subprocess.call(['ip l d dev ' + wg_intf + ' >/dev/null'], shell=True) - return None - - ### - ## find the diffs between effective config an new config - ### - c_eff = Config() - c_eff.set_level('interfaces wireguard') - - ### link status up/down aka interface disable - - for intf in c['interfaces']: - if not c['interfaces'][intf]['status'] == 'delete': - if c['interfaces'][intf]['state'] == 'disable': - sl.syslog(sl.LOG_NOTICE, "disable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' down ' + ' &>/dev/null'], shell=True) - else: - sl.syslog(sl.LOG_NOTICE, "enable interface " + intf) - subprocess.call(['ip l s dev ' + intf + ' up ' + ' &>/dev/null'], shell=True) - - ### deletion of a specific interface - for intf in c['interfaces']: - if c['interfaces'][intf]['status'] == 'delete': - sl.syslog(sl.LOG_NOTICE, "removing interface " + intf) - subprocess.call(['ip l d dev ' + intf + ' &>/dev/null'], shell=True) - - ### peer deletion - peer_eff = c_eff.list_effective_nodes( intf + ' peer') - peer_cnf = [] - try: - for p in c['interfaces'][intf]['peer']: - peer_cnf.append(p) - except KeyError: - pass - - peer_rem = list(set(peer_eff) - set(peer_cnf)) - for p in peer_rem: - pkey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - remove_peer(intf, pkey) - - ### peer pubkey update - ### wg identifies peers by its pubky, so we have to remove the peer first - ### it will recreated it then below with the new key from the cli config - for p in peer_eff: - if p in peer_cnf: - ekey = c_eff.return_effective_value( intf + ' peer ' + p +' pubkey') - nkey = c['interfaces'][intf]['peer'][p]['pubkey'] - if nkey != ekey: - sl.syslog(sl.LOG_NOTICE, "peer " + p + ' changed pubkey from ' + ekey + 'to key ' + nkey + ' on interface ' + intf) - remove_peer(intf, ekey) - - ### new config - if c['interfaces'][intf]['status'] == 'create': - if not os.path.exists(pk): - raise ConfigError("No keys found, generate them by executing: \'run generate wireguard keypair\'") - - subprocess.call(['ip l a dev ' + intf + ' type wireguard 2>/dev/null'], shell=True) - for addr in c['interfaces'][intf]['addr']: - add_addr(intf, addr) - - subprocess.call(['ip l set up dev ' + intf + ' mtu ' + c['interfaces'][intf]['mtu'] + ' &>/dev/null'], shell=True) - configure_interface(c, intf) - - ### config updates - if c['interfaces'][intf]['status'] == 'exists': - ### IP address change - addr_eff = c_eff.return_effective_values(intf + ' address') - addr_rem = list(set(addr_eff) - set(c['interfaces'][intf]['addr'])) - addr_add = list(set(c['interfaces'][intf]['addr']) - set(addr_eff)) - - if len(addr_rem) != 0: - for addr in addr_rem: - del_addr(intf, addr) - - if len(addr_add) != 0: - for addr in addr_add: - add_addr(intf, addr) - - ## mtu update - mtu = c['interfaces'][intf]['mtu'] - if mtu != 1420: - sl.syslog(sl.LOG_NOTICE, "setting mtu to " + mtu + " on " + intf) - subprocess.call(['ip l set mtu ' + mtu + ' dev ' + intf + ' &>/dev/null'], shell=True) - - - ### persistent-keepalive - for p in c['interfaces'][intf]['peer']: - val_eff = "" - val = "" - - try: - val = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] - except KeyError: - pass - - if c_eff.exists_effective(intf + ' peer ' + p + ' persistent-keepalive'): - val_eff = c_eff.return_effective_value(intf + ' peer ' + p + ' persistent-keepalive') - - ### disable keepalive - if val_eff and not val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = 0 - - ### set new keepalive value - if not val_eff and val: - c['interfaces'][intf]['peer'][p]['persistent-keepalive'] = val - - ## wg command call - configure_interface(c, intf) - - ### ifalias for snmp from description - if c['interfaces'][intf]['status'] != 'delete': - descr_eff = c_eff.return_effective_value(intf + ' description') - cnf_descr = c['interfaces'][intf]['descr'] - if descr_eff != cnf_descr: - with open('/sys/class/net/' + str(intf) + '/ifalias', 'w') as fh: - fh.write(str(cnf_descr)) - -def configure_interface(c, intf): - for p in c['interfaces'][intf]['peer']: - ## config init for wg call - wg_config = { - 'interface' : intf, - 'port' : 0, - 'private-key' : pk, - 'pubkey' : '', - 'psk' : '/dev/null', - 'allowed-ips' : [], - 'fwmark' : 0x00, - 'endpoint' : None, - 'keepalive' : 0 - } - - ## mandatory settings - wg_config['pubkey'] = c['interfaces'][intf]['peer'][p]['pubkey'] - wg_config['allowed-ips'] = c['interfaces'][intf]['peer'][p]['allowed-ips'] - - ## optional settings - # listen-port - if c['interfaces'][intf]['lport']: - wg_config['port'] = c['interfaces'][intf]['lport'] - - ## fwmark - if c['interfaces'][intf]['fwmark']: - wg_config['fwmark'] = c['interfaces'][intf]['fwmark'] - - ## endpoint - if c['interfaces'][intf]['peer'][p]['endpoint']: - wg_config['endpoint'] = c['interfaces'][intf]['peer'][p]['endpoint'] - - ## persistent-keepalive - if 'persistent-keepalive' in c['interfaces'][intf]['peer'][p]: - wg_config['keepalive'] = c['interfaces'][intf]['peer'][p]['persistent-keepalive'] - - ## preshared-key - is only read from a file, it's called via sudo redirection doesn't work either - if 'psk' in c['interfaces'][intf]['peer'][p]: - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['interfaces'][intf]['peer'][p]['psk'])) - os.umask(old_umask) - wg_config['psk'] = psk_file - - ### assemble wg command - cmd = "sudo wg set " + intf - cmd += " listen-port " + str(wg_config['port']) - cmd += " fwmark " + str(wg_config['fwmark']) - cmd += " private-key " + wg_config['private-key'] - cmd += " peer " + wg_config['pubkey'] - cmd += " preshared-key " + wg_config['psk'] - cmd += " allowed-ips " - for ap in wg_config['allowed-ips']: - if ap != wg_config['allowed-ips'][-1]: - cmd += ap + "," - else: - cmd += ap - - if wg_config['endpoint']: - cmd += " endpoint " + wg_config['endpoint'] - - if wg_config['keepalive'] != 0: - cmd += " persistent-keepalive " + wg_config['keepalive'] - else: - cmd += " persistent-keepalive 0" - - sl.syslog(sl.LOG_NOTICE, cmd) - #print (cmd) - subprocess.call([cmd], shell=True) - """ remove psk_file """ - if os.path.exists(psk_file): - os.remove(psk_file) - -def add_addr(intf, addr): - # see https://phabricator.vyos.net/T949 - ret = subprocess.call(['ip a a dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - sl.syslog(sl.LOG_NOTICE, "ip a a dev " + intf + " " + addr) - -def del_addr(intf, addr): - ret = subprocess.call(['ip a d dev ' + intf + ' ' + addr + ' &>/dev/null'], shell=True) - sl.syslog(sl.LOG_NOTICE, "ip a d dev " + intf + " " + addr) - -def remove_peer(intf, peer_key): - cmd = 'sudo wg set ' + str(intf) + ' peer ' + peer_key + ' remove &>/dev/null' - ret = subprocess.call([cmd], shell=True) - sl.syslog(sl.LOG_NOTICE, "peer " + peer_key + " removed from " + intf) - -if __name__ == '__main__': - try: - check_kmod() - c = get_config() - verify(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) -- cgit v1.2.3 From e667ffa7721bca0672f53c2fc70accdeeaa3c72a Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Thu, 1 Aug 2019 17:49:46 +0200 Subject: T786: Rename tagNode environment variable VALUE to VYOS_TAGNODE_VALUE --- scripts/build-command-templates | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build-command-templates b/scripts/build-command-templates index a7312f77b..ba80eadb2 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -227,7 +227,7 @@ def make_node_def(props): if "owner" in props: if "tag" in props: - node_def += "end: sudo sh -c \"VALUE='$VAR(@)' {0}\"\n".format(props["owner"]) + node_def += "end: sudo sh -c \"VYOS_TAGNODE_VALUE='$VAR(@)' {0}\"\n".format(props["owner"]) else: node_def += "end: sudo sh -c \"{0}\"\n".format(props["owner"]) -- cgit v1.2.3 From 0faeedf5c381659d62164ee503127bca0b6897fd Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Aug 2019 11:33:35 +0200 Subject: [bridge] T1156: first working implementation using Python and XML --- Makefile | 1 + interface-definitions/interfaces-bridge.xml | 245 ++++++++++++++++++++++++++++ python/vyos/configinterface.py | 77 +++++++++ src/conf_mode/interface-bridge.py | 227 ++++++++++++++++++++++++++ 4 files changed, 550 insertions(+) create mode 100644 interface-definitions/interfaces-bridge.xml create mode 100644 python/vyos/configinterface.py create mode 100755 src/conf_mode/interface-bridge.py diff --git a/Makefile b/Makefile index 063e9b009..89b83d4f4 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,7 @@ interface_definitions: # XXX: delete top level node.def's that now live in other packages rm -f $(TMPL_DIR)/firewall/node.def rm -f $(TMPL_DIR)/interfaces/node.def + rm -f $(TMPL_DIR)/interfaces/bridge/node.tag/ip/node.def rm -f $(TMPL_DIR)/protocols/node.def rm -f $(TMPL_DIR)/protocols/static/node.def rm -f $(TMPL_DIR)/system/node.def diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml new file mode 100644 index 000000000..a5f2df5b5 --- /dev/null +++ b/interface-definitions/interfaces-bridge.xml @@ -0,0 +1,245 @@ + + + + + + + Bridge interface name + 310 + + ^br[0-9]+$ + + Bridge interface must be named brN + + brN + Bridge interface name + + + + + + IP address + 320 + + ipv4net + IPv4 address and prefix length + + + ipv6net + IPv6 address and prefix length + + + dhcp + Dynamic Host Configuration Protocol + + + dhcpv6 + Dynamic Host Configuration Protocol for IPv6 + + + + + + + Interval addresses are retained + + 0 + Disable retaining address in bridge (always flood) + + + 10-1000000 + Address aging time for bridge seconds (default 300) + + + + + + + + + Interface description + + ^.{1,256}$ + + Interface description too long (limit 256 characters) + + + + + DHCP options + + + + + DHCP client identifier + + + + + DHCP client host name (overrides the system host name) + + + + + + + DHCPv6 options + 319 + + + + + Acquire only config parameters, no address + + + + + + IPv6 "temporary" address + + + + + + + + Ignore link state changes + + + + + + Disable this bridge interface + + + + + Forwarding delay + + 0-200 + Spanning Tree Protocol forwarding delay in seconds (default 15) + + + + + Forwarding delay must be between 0 and 200 seconds + + + + + Hello packet advertisment interval + + 1-10 + Spanning Tree Protocol hello advertisement interval in seconds (default 2) + + + + + Bridge Hello interval must be between 1 and 10 seconds + + + + + IGMP snooping settings + + + + + Enable or disable IGMP querier + + enable disable + + + enable + Enable IGMP querier + + + disable + Disable IGMP querier + + + (enable|disable) + + + + + + + + + + ARP cache entry timeout in seconds + + 1-86400 + ARP cache entry timout in seconds (default 30) + + + + + Bridge max aging value must be between 6 and 86400 seconds + + + + + + + Media Access Control (MAC) address + + h:h:h:h:h:h + Hardware (MAC) address + + + + + + + + + Interval at which neighbor bridges are removed + + 1-40 + Bridge maximum aging time in seconds (default 20) + + + + + Bridge max aging value must be between 1 and 40 seconds + + + + + Priority for this bridge + + 0-65535 + Bridge priority (default 32768) + + + + + Bridge priority must be between 0 and 65535 (multiples of 4096) + + + + + Enable spanning tree protocol + + true false + + + true + Enable Spanning Tree Protocol + + + false + Disable Spanning Tree Protocol + + + (true|false) + + + + + + + + diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py new file mode 100644 index 000000000..b0d766b9c --- /dev/null +++ b/python/vyos/configinterface.py @@ -0,0 +1,77 @@ +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import os + +def set_description(intf, desc): + """ + Sets the interface secription reported usually by SNMP + """ + with open('/sys/class/net/' + intf + '/ifalias', 'w') as f: + f.write(desc) + + +def set_arp_cache_timeout(intf, tmoMS): + """ + Configure the ARP cache entry timeout in milliseconds + """ + with open('/proc/sys/net/ipv4/neigh/' + intf + '/base_reachable_time_ms', 'w') as f: + f.write(tmoMS) + +def set_multicast_querier(intf, enable): + """ + Sets whether the bridge actively runs a multicast querier or not. When a + bridge receives a 'multicast host membership' query from another network host, + that host is tracked based on the time that the query was received plus the + multicast query interval time. + + use enable=1 to enable or enable=0 to disable + """ + + if int(enable) >= 0 and int(enable) <= 1: + with open('/sys/devices/virtual/net/' + intf + '/bridge/multicast_querier', 'w') as f: + f.write(str(enable)) + else: + raise ValueError("malformed configuration string on interface {}: enable={}".format(intf, enable)) + +def set_link_detect(intf, enable): + """ + 0 - Allow packets to be received for the address on this interface + even if interface is disabled or no carrier. + + 1 - Ignore packets received if interface associated with the incoming + address is down. + + 2 - Ignore packets received if interface associated with the incoming + address is down or has no carrier. + + Kernel Source: Documentation/networking/ip-sysctl.txt + """ + + # Note can't use sysctl it is broken for vif name because of dots + # link_filter values: + # 0 - always receive + # 1 - ignore receive if admin_down + # 2 - ignore receive if admin_down or link down + + with open('/proc/sys/net/ipv4/conf/' + intf + '/link_filter', 'w') as f: + if enable == True or enable == 1: + f.write('2') + if os.path.isfile('/usr/bin/vtysh'): + os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "link-detect"'.format(intf)) + else: + f.write('1') + if os.path.isfile('/usr/bin/vtysh'): + os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "no link-detect"'.format(intf)) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py new file mode 100755 index 000000000..f7f70b15d --- /dev/null +++ b/src/conf_mode/interface-bridge.py @@ -0,0 +1,227 @@ +#!/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 . +# +# + +import os +import sys +import copy +import subprocess + +import vyos.configinterface as VyIfconfig + +from vyos.config import Config +from vyos import ConfigError + +default_config_data = { + 'address': [], + 'aging': '300', + 'br_name': '', + 'description': '', + 'deleted': False, + 'dhcp_client_id': '', + 'dhcp_hostname': '', + 'dhcpv6_parameters_only': False, + 'dhcpv6_temporary': False, + 'disable': False, + 'disable_link_detect': False, + 'forwarding_delay': '15', + 'hello_time': '2', + 'igmp_querier': 0, + 'arp_cache_timeout_ms': '30000', + 'mac' : '', + 'max_age': '20', + 'priority': '32768', + 'stp': 'off' +} + +def subprocess_cmd(command): + process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) + proc_stdout = process.communicate()[0].strip() + print(proc_stdout) + +def get_config(): + bridge = copy.deepcopy(default_config_data) + conf = Config() + + # determine tagNode instance + try: + bridge['br_name'] = os.environ['VYOS_TAGNODE_VALUE'] + print("Executing script for interface: " + bridge['br_name']) + except KeyError as E: + print("Interface not specified") + + # Check if bridge has been removed + if not conf.exists('interfaces bridge ' + bridge['br_name']): + bridge['deleted'] = True + return bridge + + # set new configuration level + conf.set_level('interfaces bridge ' + bridge['br_name']) + + # retrieve configured interface addresses + if conf.exists('address'): + bridge['address'] = conf.return_values('address') + + # retrieve aging - how long addresses are retained + if conf.exists('aging'): + bridge['aging'] = conf.return_value('aging') + + # retrieve interface description + if conf.exists('description'): + bridge['description'] = conf.return_value('description') + + # DHCP client identifier + if conf.exists('dhcp-options client-id'): + bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id') + + # DHCP client hostname + if conf.exists('dhcp-options host-name'): + bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name') + + # DHCPv6 acquire only config parameters, no address + if conf.exists('dhcpv6-options parameters-only'): + bridge['dhcpv6_parameters_only'] = True + + # DHCPv6 IPv6 "temporary" address + if conf.exists('dhcpv6-options temporary'): + bridge['dhcpv6_temporary'] = True + + # Disable this bridge interface + if conf.exists('disable'): + bridge['disable'] = True + + # Ignore link state changes + if conf.exists('disable-link-detect'): + bridge['disable_link_detect'] = True + + # Forwarding delay + if conf.exists('forwarding-delay'): + bridge['forwarding_delay'] = conf.return_value('forwarding-delay') + + # Hello packet advertisment interval + if conf.exists('hello-time'): + bridge['hello_time'] = conf.return_value('hello-time') + + # Enable or disable IGMP querier + if conf.exists('igmp-snooping querier'): + tmp = conf.return_value('igmp-snooping querier') + if tmp == "enable": + bridge['igmp_querier'] = 1 + + # ARP cache entry timeout in seconds + if conf.exists('ip arp-cache-timeout'): + tmp = 1000 * int(conf.return_value('ip arp-cache-timeout')) + bridge['arp_cache_timeout_ms'] = str(tmp) + + # Media Access Control (MAC) address + if conf.exists('mac'): + bridge['mac'] = conf.return_value('mac') + + # Interval at which neighbor bridges are removed + if conf.exists('max-age'): + bridge['max_age'] = conf.return_value('max-age') + + # Priority for this bridge + if conf.exists('priority'): + bridge['priority'] = conf.return_value('priority') + + # Enable spanning tree protocol + if conf.exists('stp'): + tmp = conf.return_value('stp') + if tmp == "true": + bridge['stp'] = 'on' + + return bridge + +def verify(bridge): + if bridge is None: + return None + + return None + +def generate(bridge): + if bridge is None: + return None + + return None + +def apply(bridge): + if bridge is None: + return None + + if bridge['deleted']: + # bridges need to be shutdown first + os.system("ip link set dev {0} down".format(bridge['br_name'])) + # delete bridge + os.system("brctl delbr {0}".format(bridge['br_name'])) + else: + # create bridge if it does not exist + if not os.path.exists("/sys/class/net/" + bridge['br_name']): + os.system("brctl addbr {0}".format(bridge['br_name'])) + + # assemble bridge configuration + # configuration is passed via subprocess to brctl + cmd = '' + + # set ageing time + cmd += 'brctl setageing {0} {1}'.format(bridge['br_name'], bridge['aging']) + cmd += ' && ' + + # set bridge forward delay + cmd += 'brctl setfd {0} {1}'.format(bridge['br_name'], bridge['forwarding_delay']) + cmd += ' && ' + + # set hello time + cmd += 'brctl sethello {0} {1}'.format(bridge['br_name'], bridge['hello_time']) + cmd += ' && ' + + # set max message age + cmd += 'brctl setmaxage {0} {1}'.format(bridge['br_name'], bridge['max_age']) + cmd += ' && ' + + # set bridge priority + cmd += 'brctl setbridgeprio {0} {1}'.format(bridge['br_name'], bridge['priority']) + cmd += ' && ' + + # turn stp on/off + cmd += 'brctl stp {0} {1}'.format(bridge['br_name'], bridge['stp']) + + subprocess_cmd(cmd) + + # update interface description used e.g. within SNMP + VyIfconfig.set_description(bridge['br_name'], bridge['description']) + + # Ignore link state changes? + VyIfconfig.set_link_detect(bridge['br_name'], bridge['disable_link_detect']) + + # enable or disable IGMP querier + VyIfconfig.set_multicast_querier(bridge['br_name'], bridge['igmp_querier']) + + # ARP cache entry timeout in seconds + VyIfconfig.set_arp_cache_timeout(bridge['br_name'], bridge['arp_cache_timeout_ms']) + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + sys.exit(1) -- cgit v1.2.3 From f9654c1c91de4c8b43edaf7571c696373deeef15 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Aug 2019 12:16:03 +0200 Subject: [bridge] T1156: add configuration migration script --- src/migration-scripts/interface/0-to-1 | 82 ++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100755 src/migration-scripts/interface/0-to-1 diff --git a/src/migration-scripts/interface/0-to-1 b/src/migration-scripts/interface/0-to-1 new file mode 100755 index 000000000..1c6119d86 --- /dev/null +++ b/src/migration-scripts/interface/0-to-1 @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +# Change syntax of bridge interface +# - move interface based bridge-group to actual bridge (de-nest) +# - make stp and igmp-snooping nodes valueless +# https://phabricator.vyos.net/T1556 + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'bridge'] + +# +# make stp and igmp-snooping nodes valueless +# +for br in config.list_nodes(base): + # STP: check if enabled + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) + + + # igmp-snooping: check if enabled + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp-snooping', 'querier'], value=None) + +# +# move interface based bridge-group to actual bridge (de-nest) +# +bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless'] +for type in bridge_types: + if not config.exists(['interfaces', type]): + continue + + for intf in config.list_nodes(['interfaces', type]): + # check if bridge-group exists + if config.exists(['interfaces', type, intf, 'bridge-group']): + bridge = config.return_value(['interfaces', type, intf, 'bridge-group', 'bridge']) + + # create new bridge member interface + config.set(base + [bridge, 'member', 'interface', intf]) + # format as tag node to avoid loading problems + config.set_tag(base + [bridge, 'member', 'interface']) + + # cost: migrate if configured + if config.exists(['interfaces', type, intf, 'bridge-group', 'cost']): + cost = config.return_value(['interfaces', type, intf, 'bridge-group', 'cost']) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost) + + if config.exists(['interfaces', type, intf, 'bridge-group', 'priority']): + priority = config.return_value(['interfaces', type, intf, 'bridge-group', 'priority']) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority) + + # Delete the old bridge-group assigned to an interface + config.delete(['interfaces', type, intf, 'bridge-group']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From c40d6e8d6dc7b513dca666e7dcd1cab42e0fde31 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Aug 2019 12:34:34 +0200 Subject: [list-interfaces] support listing interfaces which can be bridged --- src/completion/list_interfaces.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/completion/list_interfaces.py b/src/completion/list_interfaces.py index a4968c52f..66432af19 100755 --- a/src/completion/list_interfaces.py +++ b/src/completion/list_interfaces.py @@ -10,6 +10,7 @@ parser = argparse.ArgumentParser() group = parser.add_mutually_exclusive_group() group.add_argument("-t", "--type", type=str, help="List interfaces of specific type") group.add_argument("-b", "--broadcast", action="store_true", help="List all broadcast interfaces") +group.add_argument("-br", "--bridgeable", action="store_true", help="List all bridgeable interfaces") args = parser.parse_args() @@ -25,6 +26,14 @@ elif args.broadcast: bridge = vyos.interfaces.list_interfaces_of_type("bridge") bond = vyos.interfaces.list_interfaces_of_type("bonding") interfaces = eth + bridge + bond +elif args.bridgeable: + eth = vyos.interfaces.list_interfaces_of_type("ethernet") + bond = vyos.interfaces.list_interfaces_of_type("bonding") + l2tpv3 = vyos.interfaces.list_interfaces_of_type("l2tpv3") + openvpn = vyos.interfaces.list_interfaces_of_type("openvpn") + vxlan = vyos.interfaces.list_interfaces_of_type("vxlan") + wireless = vyos.interfaces.list_interfaces_of_type("wireless") + interfaces = eth + bond + l2tpv3 + openvpn + vxlan + wireless else: interfaces = vyos.interfaces.list_interfaces() -- cgit v1.2.3 From 5b836709a15e4f6a8775e5dc26609febd5bc2480 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Fri, 2 Aug 2019 17:27:25 +0200 Subject: [bridge] T1156: support adding and removing bridge member interfaces This is the new syntax bridge br0 { member { interface eth0 { cost 10 } interface eth1 { cost 11 } } } --- interface-definitions/interfaces-bridge.xml | 76 +++++++++++++++---------- python/vyos/configinterface.py | 8 +++ src/conf_mode/interface-bridge.py | 86 +++++++++++++++++++++++------ src/migration-scripts/interface/0-to-1 | 82 --------------------------- src/migration-scripts/interfaces/0-to-1 | 82 +++++++++++++++++++++++++++ 5 files changed, 207 insertions(+), 127 deletions(-) delete mode 100755 src/migration-scripts/interface/0-to-1 create mode 100755 src/migration-scripts/interfaces/0-to-1 diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index a5f2df5b5..af19d9438 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -51,7 +51,8 @@ Address aging time for bridge seconds (default 300) - + + @@ -146,20 +147,7 @@ Enable or disable IGMP querier - - enable disable - - - enable - Enable IGMP querier - - - disable - Disable IGMP querier - - - (enable|disable) - + @@ -206,6 +194,49 @@ Bridge max aging value must be between 1 and 40 seconds + + + Bridge member interfaces + + + + + Member interface name + + + + + + + + Bridge port cost + + 1-65535 + Path cost value for Spanning Tree Protocol + + + + + Path cost value must be between 1 and 65535 + + + + + Bridge port priority + + 0-63 + Bridge port priority + + + + + Port priority value must be between 0 and 63 + + + + + + Priority for this bridge @@ -222,20 +253,7 @@ Enable spanning tree protocol - - true false - - - true - Enable Spanning Tree Protocol - - - false - Disable Spanning Tree Protocol - - - (true|false) - + diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py index b0d766b9c..37b6b92c1 100644 --- a/python/vyos/configinterface.py +++ b/python/vyos/configinterface.py @@ -15,6 +15,14 @@ import os +def set_mac_address(intf, addr): + """ + Configure interface mac address using iproute2 command + + NOTE: mac address should be validated here??? + """ + os.system('ip link set {} address {}'.format(intf, addr)) + def set_description(intf, desc): """ Sets the interface secription reported usually by SNMP diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index f7f70b15d..637c58a5e 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -44,6 +44,8 @@ default_config_data = { 'arp_cache_timeout_ms': '30000', 'mac' : '', 'max_age': '20', + 'member': [], + 'member_remove': [], 'priority': '32768', 'stp': 'off' } @@ -53,6 +55,10 @@ def subprocess_cmd(command): proc_stdout = process.communicate()[0].strip() print(proc_stdout) +def diff(first, second): + second = set(second) + return [item for item in first if item not in second] + def get_config(): bridge = copy.deepcopy(default_config_data) conf = Config() @@ -60,7 +66,6 @@ def get_config(): # determine tagNode instance try: bridge['br_name'] = os.environ['VYOS_TAGNODE_VALUE'] - print("Executing script for interface: " + bridge['br_name']) except KeyError as E: print("Interface not specified") @@ -118,9 +123,7 @@ def get_config(): # Enable or disable IGMP querier if conf.exists('igmp-snooping querier'): - tmp = conf.return_value('igmp-snooping querier') - if tmp == "enable": - bridge['igmp_querier'] = 1 + bridge['igmp_querier'] = 1 # ARP cache entry timeout in seconds if conf.exists('ip arp-cache-timeout'): @@ -135,15 +138,35 @@ def get_config(): if conf.exists('max-age'): bridge['max_age'] = conf.return_value('max-age') + # Determine bridge member interface (currently configured) + for intf in conf.list_nodes('member interface'): + iface = { + 'name': intf, + 'cost': '', + 'priority': '' + } + + if conf.exists('member interface {} cost'.format(intf)): + iface['cost'] = conf.return_value('member interface {} cost'.format(intf)) + + if conf.exists('member interface {} priority'.format(intf)): + iface['priority'] = conf.return_value('member interface {} priority'.format(intf)) + + bridge['member'].append(iface) + + # Determine bridge member interface (currently effective) - to determine which interfaces + # need to be removed from the bridge + eff_intf = conf.list_effective_nodes('member interface') + act_intf = conf.list_nodes('member interface') + bridge['member_remove'] = diff(eff_intf, act_intf) + # Priority for this bridge if conf.exists('priority'): bridge['priority'] = conf.return_value('priority') # Enable spanning tree protocol if conf.exists('stp'): - tmp = conf.return_value('stp') - if tmp == "true": - bridge['stp'] = 'on' + bridge['stp'] = 'on' return bridge @@ -151,6 +174,9 @@ def verify(bridge): if bridge is None: return None + # validate agains other bridge interfaces that the interface is not assigned + # to another bridge + return None def generate(bridge): @@ -165,43 +191,71 @@ def apply(bridge): if bridge['deleted']: # bridges need to be shutdown first - os.system("ip link set dev {0} down".format(bridge['br_name'])) + os.system("ip link set dev {} down".format(bridge['br_name'])) # delete bridge - os.system("brctl delbr {0}".format(bridge['br_name'])) + os.system("brctl delbr {}".format(bridge['br_name'])) else: # create bridge if it does not exist if not os.path.exists("/sys/class/net/" + bridge['br_name']): - os.system("brctl addbr {0}".format(bridge['br_name'])) + os.system("brctl addbr {}".format(bridge['br_name'])) # assemble bridge configuration # configuration is passed via subprocess to brctl cmd = '' # set ageing time - cmd += 'brctl setageing {0} {1}'.format(bridge['br_name'], bridge['aging']) + cmd += 'brctl setageing {} {}'.format(bridge['br_name'], bridge['aging']) cmd += ' && ' # set bridge forward delay - cmd += 'brctl setfd {0} {1}'.format(bridge['br_name'], bridge['forwarding_delay']) + cmd += 'brctl setfd {} {}'.format(bridge['br_name'], bridge['forwarding_delay']) cmd += ' && ' # set hello time - cmd += 'brctl sethello {0} {1}'.format(bridge['br_name'], bridge['hello_time']) + cmd += 'brctl sethello {} {}'.format(bridge['br_name'], bridge['hello_time']) cmd += ' && ' # set max message age - cmd += 'brctl setmaxage {0} {1}'.format(bridge['br_name'], bridge['max_age']) + cmd += 'brctl setmaxage {} {}'.format(bridge['br_name'], bridge['max_age']) cmd += ' && ' # set bridge priority - cmd += 'brctl setbridgeprio {0} {1}'.format(bridge['br_name'], bridge['priority']) + cmd += 'brctl setbridgeprio {} {}'.format(bridge['br_name'], bridge['priority']) cmd += ' && ' # turn stp on/off - cmd += 'brctl stp {0} {1}'.format(bridge['br_name'], bridge['stp']) + cmd += 'brctl stp {} {}'.format(bridge['br_name'], bridge['stp']) + + for intf in bridge['member_remove']: + # remove interface from bridge + cmd += ' && ' + cmd += 'brctl delif {} {}'.format(bridge['br_name'], intf) + + for intf in bridge['member']: + # add interface to bridge + # but only if it is not yet member of this bridge + if not os.path.exists('/sys/devices/virtual/net/' + bridge['br_name'] + '/brif/' + intf['name']): + cmd += ' && ' + cmd += 'brctl addif {} {}'.format(bridge['br_name'], intf['name']) + + # set bridge port cost + if intf['cost']: + cmd += ' && ' + cmd += 'brctl setpathcost {} {} {}'.format(bridge['br_name'], intf['name'], intf['cost']) + + # set bridge port priority + if intf['priority']: + cmd += ' && ' + cmd += 'brctl setportprio {} {} {}'.format(bridge['br_name'], intf['name'], intf['priority']) subprocess_cmd(cmd) + # Change interface MAC address + if bridge['mac']: + VyIfconfig.set_mac_address(bridge['br_name'], bridge['mac']) + else: + print("TODO: change mac mac address to the autoselected one based on member interfaces" + # update interface description used e.g. within SNMP VyIfconfig.set_description(bridge['br_name'], bridge['description']) diff --git a/src/migration-scripts/interface/0-to-1 b/src/migration-scripts/interface/0-to-1 deleted file mode 100755 index 1c6119d86..000000000 --- a/src/migration-scripts/interface/0-to-1 +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 - -# Change syntax of bridge interface -# - move interface based bridge-group to actual bridge (de-nest) -# - make stp and igmp-snooping nodes valueless -# https://phabricator.vyos.net/T1556 - -import sys - -from vyos.configtree import ConfigTree - -if (len(sys.argv) < 1): - print("Must specify file name!") - sys.exit(1) - -file_name = sys.argv[1] - -with open(file_name, 'r') as f: - config_file = f.read() - -config = ConfigTree(config_file) -base = ['interfaces', 'bridge'] - -# -# make stp and igmp-snooping nodes valueless -# -for br in config.list_nodes(base): - # STP: check if enabled - stp_val = config.return_value(base + [br, 'stp']) - # STP: delete node with old syntax - config.delete(base + [br, 'stp']) - # STP: set new node - if enabled - if stp_val == "true": - config.set(base + [br, 'stp'], value=None) - - - # igmp-snooping: check if enabled - igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: delete node with old syntax - config.delete(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: set new node - if enabled - if igmp_val == "enable": - config.set(base + [br, 'igmp-snooping', 'querier'], value=None) - -# -# move interface based bridge-group to actual bridge (de-nest) -# -bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless'] -for type in bridge_types: - if not config.exists(['interfaces', type]): - continue - - for intf in config.list_nodes(['interfaces', type]): - # check if bridge-group exists - if config.exists(['interfaces', type, intf, 'bridge-group']): - bridge = config.return_value(['interfaces', type, intf, 'bridge-group', 'bridge']) - - # create new bridge member interface - config.set(base + [bridge, 'member', 'interface', intf]) - # format as tag node to avoid loading problems - config.set_tag(base + [bridge, 'member', 'interface']) - - # cost: migrate if configured - if config.exists(['interfaces', type, intf, 'bridge-group', 'cost']): - cost = config.return_value(['interfaces', type, intf, 'bridge-group', 'cost']) - # set new node - config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost) - - if config.exists(['interfaces', type, intf, 'bridge-group', 'priority']): - priority = config.return_value(['interfaces', type, intf, 'bridge-group', 'priority']) - # set new node - config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority) - - # Delete the old bridge-group assigned to an interface - config.delete(['interfaces', type, intf, 'bridge-group']) - - try: - with open(file_name, 'w') as f: - f.write(config.to_string()) - except OSError as e: - print("Failed to save the modified config: {}".format(e)) - sys.exit(1) diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 new file mode 100755 index 000000000..1c6119d86 --- /dev/null +++ b/src/migration-scripts/interfaces/0-to-1 @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 + +# Change syntax of bridge interface +# - move interface based bridge-group to actual bridge (de-nest) +# - make stp and igmp-snooping nodes valueless +# https://phabricator.vyos.net/T1556 + +import sys + +from vyos.configtree import ConfigTree + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['interfaces', 'bridge'] + +# +# make stp and igmp-snooping nodes valueless +# +for br in config.list_nodes(base): + # STP: check if enabled + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) + + + # igmp-snooping: check if enabled + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp-snooping', 'querier'], value=None) + +# +# move interface based bridge-group to actual bridge (de-nest) +# +bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless'] +for type in bridge_types: + if not config.exists(['interfaces', type]): + continue + + for intf in config.list_nodes(['interfaces', type]): + # check if bridge-group exists + if config.exists(['interfaces', type, intf, 'bridge-group']): + bridge = config.return_value(['interfaces', type, intf, 'bridge-group', 'bridge']) + + # create new bridge member interface + config.set(base + [bridge, 'member', 'interface', intf]) + # format as tag node to avoid loading problems + config.set_tag(base + [bridge, 'member', 'interface']) + + # cost: migrate if configured + if config.exists(['interfaces', type, intf, 'bridge-group', 'cost']): + cost = config.return_value(['interfaces', type, intf, 'bridge-group', 'cost']) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost) + + if config.exists(['interfaces', type, intf, 'bridge-group', 'priority']): + priority = config.return_value(['interfaces', type, intf, 'bridge-group', 'priority']) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority) + + # Delete the old bridge-group assigned to an interface + config.delete(['interfaces', type, intf, 'bridge-group']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From 5b15c162c008958de2be9cba2cbf8f0b65bc6fb9 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sat, 3 Aug 2019 22:24:10 +0200 Subject: [bridge] T1156: interfaces can be assigned to any one bridge only --- src/conf_mode/interface-bridge.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 637c58a5e..1c9c6ec10 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -174,8 +174,16 @@ def verify(bridge): if bridge is None: return None - # validate agains other bridge interfaces that the interface is not assigned - # to another bridge + conf = Config() + for br in conf.list_nodes('interfaces bridge'): + # it makes no sense to verify ourself in this case + if br == bridge['br_name']: + continue + + for intf in bridge['member']: + tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br)) + if intf['name'] in tmp: + raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name'])) return None -- cgit v1.2.3 From 74cd9b982a3e965d422bce84375f6283088ec593 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 4 Aug 2019 21:50:10 +0200 Subject: [bridge] T1156: rename igmp-snooping node to igmp --- interface-definitions/interfaces-bridge.xml | 6 +++--- src/conf_mode/interface-bridge.py | 4 ++-- src/migration-scripts/interfaces/0-to-1 | 3 +-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index af19d9438..16fd8b14c 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -139,14 +139,14 @@ Bridge Hello interval must be between 1 and 10 seconds - + - IGMP snooping settings + Internet Group Management Protocol (IGMP) settings - Enable or disable IGMP querier + Enable IGMP querier diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 1c9c6ec10..4f1cbd17c 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -121,8 +121,8 @@ def get_config(): if conf.exists('hello-time'): bridge['hello_time'] = conf.return_value('hello-time') - # Enable or disable IGMP querier - if conf.exists('igmp-snooping querier'): + # Enable Internet Group Management Protocol (IGMP) querier + if conf.exists('igmp querier'): bridge['igmp_querier'] = 1 # ARP cache entry timeout in seconds diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 index 1c6119d86..b8e190f2c 100755 --- a/src/migration-scripts/interfaces/0-to-1 +++ b/src/migration-scripts/interfaces/0-to-1 @@ -33,14 +33,13 @@ for br in config.list_nodes(base): if stp_val == "true": config.set(base + [br, 'stp'], value=None) - # igmp-snooping: check if enabled igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) # igmp-snooping: delete node with old syntax config.delete(base + [br, 'igmp-snooping', 'querier']) # igmp-snooping: set new node - if enabled if igmp_val == "enable": - config.set(base + [br, 'igmp-snooping', 'querier'], value=None) + config.set(base + [br, 'igmp', 'querier'], value=None) # # move interface based bridge-group to actual bridge (de-nest) -- cgit v1.2.3 From 7ce45e063265170185097fc1aef4f3f7f3bbc9f5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 4 Aug 2019 22:00:15 +0200 Subject: [bridge] T1156: remove helper script bridge_has_members.py Bridge member interface is now handled completely inside the bridge node and no longer spread accross different interface definitions. --- src/conf_mode/bridge_has_members.py | 85 ------------------------------------- 1 file changed, 85 deletions(-) delete mode 100755 src/conf_mode/bridge_has_members.py diff --git a/src/conf_mode/bridge_has_members.py b/src/conf_mode/bridge_has_members.py deleted file mode 100755 index 712a9cc46..000000000 --- a/src/conf_mode/bridge_has_members.py +++ /dev/null @@ -1,85 +0,0 @@ -#!/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 . -# -# - -import sys - -import vyos.config - -if len(sys.argv) < 2: - print("Argument (bridge interface name) is required") - sys.exit(1) -else: - bridge = sys.argv[1] - -c = vyos.config.Config() - -members = [] - - -# Check in ethernet and bonding interfaces -for p in ["interfaces ethernet", "interfaces bonding"]: - intfs = c.list_nodes(p) - for i in intfs: - intf_bridge_path = "{0} {1} bridge-group bridge".format(p, i) - if c.exists(intf_bridge_path): - intf_bridge = c.return_value(intf_bridge_path) - if intf_bridge == bridge: - members.append(i) - # Walk VLANs - for v in c.list_nodes("{0} {1} vif".format(p, i)): - vif_bridge_path = "{0} {1} vif {2} bridge-group bridge".format(p, i, v) - if c.exists(vif_bridge_path): - vif_bridge = c.return_value(vif_bridge_path) - if vif_bridge == bridge: - members.append("{0}.{1}".format(i, v)) - # Walk QinQ interfaces - for vs in c.list_nodes("{0} {1} vif-s".format(p, i)): - vifs_bridge_path = "{0} {1} vif-s {2} bridge-group bridge".format(p, i, vs) - if c.exists(vifs_bridge_path): - vifs_bridge = c.return_value(vifs_bridge_path) - if vifs_bridge == bridge: - members.append("{0}.{1}".format(i, vs)) - for vc in c.list_nodes("{0} {1} vif-s {2} vif-c".format(p, i, vs)): - vifc_bridge_path = "{0} {1} vif-s {2} vif-c {3} bridge-group bridge".format(p, i, vs, vc) - if c.exists(vifc_bridge_path): - vifc_bridge = c.return_value(vifc_bridge_path) - if vifc_bridge == bridge: - members.append("{0}.{1}.{2}".format(i, vs, vc)) - -# Check tunnel interfaces -for t in c.list_nodes("interfaces tunnel"): - tunnel_bridge_path = "interfaces tunnel {0} parameters ip bridge-group bridge".format(t) - if c.exists(tunnel_bridge_path): - intf_bridge = c.return_value(tunnel_bridge_path) - if intf_bridge == bridge: - members.append(t) - -# Check OpenVPN interfaces -for o in c.list_nodes("interfaces openvpn"): - ovpn_bridge_path = "interfaces openvpn {0} bridge-group bridge".format(o) - if c.exists(ovpn_bridge_path): - intf_bridge = c.return_value(ovpn_bridge_path) - if intf_bridge == bridge: - members.append(o) - -if members: - print("Bridge {0} cannot be deleted because some interfaces are configured as its members".format(bridge)) - print("The following interfaces are members of {0}: {1}".format(bridge, " ".join(members))) - sys.exit(1) -else: - sys.exit(0) -- cgit v1.2.3 From 8545b650f0cc1e7319744c3da57d5a74472246dc Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 4 Aug 2019 22:31:06 +0200 Subject: [bridge] T1156: validate if supplied MAC address is valid --- python/vyos/configinterface.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py index 37b6b92c1..8ff93c02f 100644 --- a/python/vyos/configinterface.py +++ b/python/vyos/configinterface.py @@ -15,12 +15,32 @@ import os +def validate_mac_address(addr): + # a mac address consits out of 6 octets + octets = len(addr.split(':')) + if octets != 6: + raise ValueError('wrong number of MAC octets: {} '.format(octets)) + + # validate against the first mac address byte if it's a multicast address + if int(addr.split(':')[0]) & 1: + raise ValueError('{} is a multicast MAC address'.format(addr)) + + # overall mac address is not allowed to be 00:00:00:00:00:00 + if sum(int(i, 16) for i in addr.split(':')) == 0: + raise ValueError('00:00:00:00:00:00 is not a valid MAC address') + + # check for VRRP mac address + if addr.split(':')[0] == '0' and addr.split(':')[1] == '0' and addr.split(':')[2] == '94' and addr.split(':')[3] == '0' and addr.split(':')[4] == '1': + raise ValueError('{} is a VRRP MAC address') + + pass + def set_mac_address(intf, addr): """ Configure interface mac address using iproute2 command - - NOTE: mac address should be validated here??? """ + validate_mac_address(addr) + os.system('ip link set {} address {}'.format(intf, addr)) def set_description(intf, desc): -- cgit v1.2.3 From 2a2e3e8cded406ce209b94f565b16e5a766ec97e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 4 Aug 2019 22:33:20 +0200 Subject: [bridge] T1156: add missing 'pass' statements --- python/vyos/configinterface.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py index 8ff93c02f..0f25c4a89 100644 --- a/python/vyos/configinterface.py +++ b/python/vyos/configinterface.py @@ -42,6 +42,7 @@ def set_mac_address(intf, addr): validate_mac_address(addr) os.system('ip link set {} address {}'.format(intf, addr)) + pass def set_description(intf, desc): """ @@ -50,6 +51,7 @@ def set_description(intf, desc): with open('/sys/class/net/' + intf + '/ifalias', 'w') as f: f.write(desc) + pass def set_arp_cache_timeout(intf, tmoMS): """ @@ -58,6 +60,8 @@ def set_arp_cache_timeout(intf, tmoMS): with open('/proc/sys/net/ipv4/neigh/' + intf + '/base_reachable_time_ms', 'w') as f: f.write(tmoMS) + pass + def set_multicast_querier(intf, enable): """ Sets whether the bridge actively runs a multicast querier or not. When a @@ -74,6 +78,8 @@ def set_multicast_querier(intf, enable): else: raise ValueError("malformed configuration string on interface {}: enable={}".format(intf, enable)) + pass + def set_link_detect(intf, enable): """ 0 - Allow packets to be received for the address on this interface @@ -103,3 +109,5 @@ def set_link_detect(intf, enable): f.write('1') if os.path.isfile('/usr/bin/vtysh'): os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "no link-detect"'.format(intf)) + + pass -- cgit v1.2.3 From 752229cf7ef080d7a5dd723e7d9b1aa13e44ecd0 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Sun, 4 Aug 2019 23:44:03 +0200 Subject: Python/VyOS validate: improve logic on is_ipv4() and is_ipv6() Previosly the check failed when a network statement was passed which contained host bits set e.g. 192.0.2.1/24. This no longer is an issue b/c this is a valid v4 address. Address is now split on / and validated. --- python/vyos/validate.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 8def0a510..8f453f85d 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -18,22 +18,24 @@ import ipaddress def is_ipv4(addr): """ - Check addr if it is an IPv4 address/network. - - Return True/False + Check addr if it is an IPv4 address/network. Returns True/False """ - if ipaddress.ip_network(addr).version == 4: + + # With the below statement we can check for IPv4 networks and host + # addresses at the same time + if ipaddress.ip_address(addr.split(r'/')[0]).version == 4: return True else: return False def is_ipv6(addr): """ - Check addr if it is an IPv6 address/network. - - Return True/False + Check addr if it is an IPv6 address/network. Returns True/False """ - if ipaddress.ip_network(addr).version == 6: + + # With the below statement we can check for IPv4 networks and host + # addresses at the same time + if ipaddress.ip_network(addr.split(r'/')[0]).version == 6: return True else: return False -- cgit v1.2.3 From 6e9a0162f84a1baca9acf0ca675ab3c574c7e297 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 5 Aug 2019 01:15:20 +0200 Subject: Python/VyOS validate: add helper to check if an address belongs to a given interface --- python/vyos/validate.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index 8f453f85d..fb7fa3051 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -40,12 +40,9 @@ def is_ipv6(addr): else: return False -def is_addr_assigned(addr): +def is_intf_addr_assigned(intf, addr): """ - Verify if the given IPv4/IPv6 address is assigned to any interface on this - system. - - Return True/False + Verify if the given IPv4/IPv6 address is assigned to specific interface """ # determine IP version (AF_INET or AF_INET6) depending on passed address @@ -53,14 +50,31 @@ def is_addr_assigned(addr): if is_ipv6(addr): addr_type = netifaces.AF_INET6 - for interface in netifaces.interfaces(): - # check if the requested address type is configured at all - if addr_type in netifaces.ifaddresses(interface).keys(): - # Check every IP address on this interface for a match - for ip in netifaces.ifaddresses(interface)[addr_type]: - # Check if it matches to the address requested - if ip['addr'] == addr: - return True + # check if the requested address type is configured at all + try: + netifaces.ifaddresses(intf) + except ValueError as e: + print(e) + return False + + if addr_type in netifaces.ifaddresses(intf).keys(): + # Check every IP address on this interface for a match + for ip in netifaces.ifaddresses(intf)[addr_type]: + # Check if it matches to the address requested + if ip['addr'] == addr: + return True + + return False + +def is_addr_assigned(addr): + """ + Verify if the given IPv4/IPv6 address is assigned to any interface + """ + + for intf in netifaces.interfaces(): + tmp = is_intf_addr_assigned(intf, addr) + if tmp == True: + return True return False -- cgit v1.2.3 From d765eef461e53241cf57bcb6b409dc6fec0efc92 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 5 Aug 2019 11:28:23 +0200 Subject: Python/VyOS validate: extend is_intf_addr_assigned() Verify if the given IPv4/IPv6 address is assigned to specific interface. It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR address 192.0.2.1/24. Used testbench: =============== 20: br0: mtu 1500 qdisc noop state DOWN group default qlen 1000 inet 192.0.2.1/24 brd 192.0.2.255 scope global br0 inet 192.0.3.1/24 brd 192.0.3.255 scope global br0 inet6 2001:db8:2::ffff/64 scope global tentative inet6 2001:db8:1::ffff/64 scope global tentative is_intf_addr_assigned('br0', '192.0.2.1/24') -> True is_intf_addr_assigned('br0', '192.0.2.1') -> True is_intf_addr_assigned('br0', '2001:db8:2::ffff/64') -> True is_intf_addr_assigned('br0', '2001:db8:2::ffff') -> True is_intf_addr_assigned('br0', '192.0.100.1/24') -> False is_intf_addr_assigned('br0', '192.0.100.1') -> False is_intf_addr_assigned('br0', '2001:db8:100::ffff/64') -> False is_intf_addr_assigned('br0', '2001:db8:100::ffff') -> False --- python/vyos/validate.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/python/vyos/validate.py b/python/vyos/validate.py index fb7fa3051..97a401423 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -42,7 +42,9 @@ def is_ipv6(addr): def is_intf_addr_assigned(intf, addr): """ - Verify if the given IPv4/IPv6 address is assigned to specific interface + Verify if the given IPv4/IPv6 address is assigned to specific interface. + It can check both a single IP address (e.g. 192.0.2.1 or a assigned CIDR + address 192.0.2.1/24. """ # determine IP version (AF_INET or AF_INET6) depending on passed address @@ -61,8 +63,28 @@ def is_intf_addr_assigned(intf, addr): # Check every IP address on this interface for a match for ip in netifaces.ifaddresses(intf)[addr_type]: # Check if it matches to the address requested - if ip['addr'] == addr: - return True + # If passed address contains a '/' indicating a normalized IP + # address we have to take this into account, too + if r'/' in addr: + prefixlen = '' + if is_ipv6(addr): + # Note that currently expanded netmasks are not supported. That means + # 2001:db00::0/24 is a valid argument while 2001:db00::0/ffff:ff00:: not. + # see https://docs.python.org/3/library/ipaddress.html + bits = bin( int(ip['netmask'].replace(':',''), 16) ).count('1') + prefixlen = '/' + str(bits) + + else: + prefixlen = '/' + str(ipaddress.IPv4Network('0.0.0.0/' + ip['netmask']).prefixlen) + + # construct temporary variable holding IPv6 address and netmask + # in CIDR notation + tmp = ip['addr'] + prefixlen + if addr == tmp: + return True + + elif ip['addr'] == addr: + return True return False -- cgit v1.2.3 From df941fdc799d8b5eba6a1780aef383bd71c9e0b7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 5 Aug 2019 11:39:59 +0200 Subject: [bridge] T1156: remove priority of address node --- interface-definitions/interfaces-bridge.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 16fd8b14c..5cb8c7c40 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -19,7 +19,6 @@ IP address - 320 ipv4net IPv4 address and prefix length -- cgit v1.2.3 From d4d47d8a39c7f6dc5eb5722fe7782262b1544321 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 5 Aug 2019 11:40:34 +0200 Subject: [bridge] T1156: add XML address constraints --- interface-definitions/interfaces-bridge.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 5cb8c7c40..4186a94a7 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -35,6 +35,10 @@ dhcpv6 Dynamic Host Configuration Protocol for IPv6 + + + (dhcp|dhcpv6) + -- cgit v1.2.3 From 61cf03e22bbd1cef574e1884e9814cc3cc464a90 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 5 Aug 2019 11:41:39 +0200 Subject: [bridge] T1156: support adding interface addresses --- python/vyos/configinterface.py | 40 +++++++++++++++++++++++++ src/conf_mode/interface-bridge.py | 61 +++++++++++++++++++++++++-------------- 2 files changed, 79 insertions(+), 22 deletions(-) diff --git a/python/vyos/configinterface.py b/python/vyos/configinterface.py index 0f25c4a89..0f5b0842c 100644 --- a/python/vyos/configinterface.py +++ b/python/vyos/configinterface.py @@ -14,6 +14,7 @@ # License along with this library. If not, see . import os +import vyos.validate def validate_mac_address(addr): # a mac address consits out of 6 octets @@ -111,3 +112,42 @@ def set_link_detect(intf, enable): os.system('/usr/bin/vtysh -c "configure terminal" -c "interface {}" -c "no link-detect"'.format(intf)) pass + +def add_interface_address(intf, addr): + """ + Configure an interface IPv4/IPv6 address + """ + if addr == "dhcp": + os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=start'.format(intf)) + elif addr == "dhcpv6": + os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --start -ifname "{}"'.format(intf)) + elif vyos.validate.is_ipv4(addr): + if not vyos.validate.is_intf_addr_assigned(intf, addr): + print("Assigning {} to {}".format(addr, intf)) + os.system('sudo ip -4 addr add "{}" broadcast + dev "{}"'.format(addr, intf)) + elif vyos.validate.is_ipv6(addr): + if not vyos.validate.is_intf_addr_assigned(intf, addr): + print("Assigning {} to {}".format(addr, intf)) + os.system('sudo ip -6 addr add "{}" dev "{}"'.format(addr, intf)) + else: + raise ConfigError('{} is not a valid interface address'.format(addr)) + + pass + +def remove_interface_address(intf, addr): + """ + Remove IPv4/IPv6 address from given interface + """ + + if addr == "dhcp": + os.system('/opt/vyatta/sbin/vyatta-interfaces.pl --dev="{}" --dhcp=stop'.format(intf)) + elif addr == "dhcpv6": + os.system('/opt/vyatta/sbin/vyatta-dhcpv6-client.pl --stop -ifname "{}"'.format(intf)) + elif vyos.validate.is_ipv4(addr): + os.system('ip -4 addr del "{}" dev "{}"'.format(addr, intf)) + elif vyos.validate.is_ipv6(addr): + os.system('ip -6 addr del "{}" dev "{}"'.format(addr, intf)) + else: + raise ConfigError('{} is not a valid interface address'.format(addr)) + + pass diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 4f1cbd17c..93eb3839c 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -28,6 +28,7 @@ from vyos import ConfigError default_config_data = { 'address': [], + 'address_remove': [], 'aging': '300', 'br_name': '', 'description': '', @@ -53,7 +54,7 @@ default_config_data = { def subprocess_cmd(command): process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True) proc_stdout = process.communicate()[0].strip() - print(proc_stdout) + pass def diff(first, second): second = set(second) @@ -154,12 +155,18 @@ def get_config(): bridge['member'].append(iface) - # Determine bridge member interface (currently effective) - to determine which interfaces - # need to be removed from the bridge + # Determine bridge member interface (currently effective) - to determine which + # interfaces is no longer assigend to the bridge and thus can be removed eff_intf = conf.list_effective_nodes('member interface') act_intf = conf.list_nodes('member interface') bridge['member_remove'] = diff(eff_intf, act_intf) + # Determine interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed from the bridge + eff_addr = conf.return_effective_values('address') + act_addr = conf.return_values('address') + bridge['address_remove'] = diff(eff_addr, act_addr) + # Priority for this bridge if conf.exists('priority'): bridge['priority'] = conf.return_value('priority') @@ -197,72 +204,75 @@ def apply(bridge): if bridge is None: return None + cmd = '' if bridge['deleted']: # bridges need to be shutdown first - os.system("ip link set dev {} down".format(bridge['br_name'])) + cmd += 'ip link set dev "{}" down'.format(bridge['br_name']) + cmd += ' && ' # delete bridge - os.system("brctl delbr {}".format(bridge['br_name'])) + cmd += 'brctl delbr "{}"'.format(bridge['br_name']) + subprocess_cmd(cmd) + else: # create bridge if it does not exist if not os.path.exists("/sys/class/net/" + bridge['br_name']): - os.system("brctl addbr {}".format(bridge['br_name'])) - - # assemble bridge configuration - # configuration is passed via subprocess to brctl - cmd = '' + # create bridge interface + cmd += 'brctl addbr "{}"'.format(bridge['br_name']) + cmd += ' && ' + # activate "UP" the interface + cmd += 'ip link set dev "{}" up'.format(bridge['br_name']) + cmd += ' && ' # set ageing time - cmd += 'brctl setageing {} {}'.format(bridge['br_name'], bridge['aging']) + cmd += 'brctl setageing "{}" "{}"'.format(bridge['br_name'], bridge['aging']) cmd += ' && ' # set bridge forward delay - cmd += 'brctl setfd {} {}'.format(bridge['br_name'], bridge['forwarding_delay']) + cmd += 'brctl setfd "{}" "{}"'.format(bridge['br_name'], bridge['forwarding_delay']) cmd += ' && ' # set hello time - cmd += 'brctl sethello {} {}'.format(bridge['br_name'], bridge['hello_time']) + cmd += 'brctl sethello "{}" "{}"'.format(bridge['br_name'], bridge['hello_time']) cmd += ' && ' # set max message age - cmd += 'brctl setmaxage {} {}'.format(bridge['br_name'], bridge['max_age']) + cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['br_name'], bridge['max_age']) cmd += ' && ' # set bridge priority - cmd += 'brctl setbridgeprio {} {}'.format(bridge['br_name'], bridge['priority']) + cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['br_name'], bridge['priority']) cmd += ' && ' # turn stp on/off - cmd += 'brctl stp {} {}'.format(bridge['br_name'], bridge['stp']) + cmd += 'brctl stp "{}" "{}"'.format(bridge['br_name'], bridge['stp']) for intf in bridge['member_remove']: # remove interface from bridge cmd += ' && ' - cmd += 'brctl delif {} {}'.format(bridge['br_name'], intf) + cmd += 'brctl delif "{}" "{}"'.format(bridge['br_name'], intf) for intf in bridge['member']: # add interface to bridge # but only if it is not yet member of this bridge if not os.path.exists('/sys/devices/virtual/net/' + bridge['br_name'] + '/brif/' + intf['name']): cmd += ' && ' - cmd += 'brctl addif {} {}'.format(bridge['br_name'], intf['name']) + cmd += 'brctl addif "{}" "{}"'.format(bridge['br_name'], intf['name']) # set bridge port cost if intf['cost']: cmd += ' && ' - cmd += 'brctl setpathcost {} {} {}'.format(bridge['br_name'], intf['name'], intf['cost']) + cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['br_name'], intf['name'], intf['cost']) # set bridge port priority if intf['priority']: cmd += ' && ' - cmd += 'brctl setportprio {} {} {}'.format(bridge['br_name'], intf['name'], intf['priority']) + cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['br_name'], intf['name'], intf['priority']) subprocess_cmd(cmd) # Change interface MAC address if bridge['mac']: VyIfconfig.set_mac_address(bridge['br_name'], bridge['mac']) - else: - print("TODO: change mac mac address to the autoselected one based on member interfaces" # update interface description used e.g. within SNMP VyIfconfig.set_description(bridge['br_name'], bridge['description']) @@ -276,6 +286,13 @@ def apply(bridge): # ARP cache entry timeout in seconds VyIfconfig.set_arp_cache_timeout(bridge['br_name'], bridge['arp_cache_timeout_ms']) + # Configure interface address(es) + for addr in bridge['address_remove']: + VyIfconfig.remove_interface_address(bridge['br_name'], addr) + + for addr in bridge['address']: + VyIfconfig.add_interface_address(bridge['br_name'], addr) + return None if __name__ == '__main__': -- cgit v1.2.3 From f8cc906b8ef3427b3a8686777d5bc2e3acbe4b7e Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Mon, 5 Aug 2019 12:28:07 +0200 Subject: [bridge] T1156: add XML completion helpers for interface address (dhcp and dhcpv6) --- interface-definitions/interfaces-bridge.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 4186a94a7..93f374c80 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -19,6 +19,9 @@ IP address + + dhcp dhcpv6 + ipv4net IPv4 address and prefix length -- cgit v1.2.3 From b4ed7280179e814b9837a0fbfa05ff8065dd8b50 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 5 Aug 2019 21:19:44 +0200 Subject: [vyos.configsession] Return the output of the external process from __run_command. --- python/vyos/configsession.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 78f332d66..1a8077edd 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -116,6 +116,7 @@ class ConfigSession(object): output = p.stdout.read().decode() if result != 0: raise ConfigSessionError(output) + return output def get_session_env(self): return self.__session_env -- cgit v1.2.3 From 9ac783472872329fe1a1683585b679a7afcc78f0 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 5 Aug 2019 21:20:54 +0200 Subject: T1431: add showConfig operation to the HTTP API. --- python/vyos/configsession.py | 8 ++++++++ src/services/vyos-http-api-server | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 1a8077edd..8626839f2 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -23,6 +23,7 @@ DELETE = '/opt/vyatta/sbin/my_delete' COMMENT = '/opt/vyatta/sbin/my_comment' COMMIT = '/opt/vyatta/sbin/my_commit' DISCARD = '/opt/vyatta/sbin/my_discard' +SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] # Default "commit via" string APP = "vyos-http-api" @@ -147,3 +148,10 @@ class ConfigSession(object): def discard(self): self.__run_command([DISCARD]) + + def show_config(self, path, format='raw'): + config_data = self.__run_command(SHOW_CONFIG + path) + + if format == 'raw': + return config_data + diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index e11eb6d52..afab9be70 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -179,6 +179,7 @@ def configure(): @app.route('/retrieve', method='POST') def get_value(): config = app.config['vyos_config'] + session = app.config['vyos_session'] api_keys = app.config['vyos_keys'] @@ -190,8 +191,11 @@ def get_value(): command = bottle.request.forms.get("data") command = json.loads(command) - op = command['op'] - path = " ".join(command['path']) + try: + op = command['op'] + path = " ".join(command['path']) + except KeyError: + return error(400, "Missing required field. \"op\" and \"path\" fields are required") try: if op == 'returnValue': @@ -200,6 +204,12 @@ def get_value(): res = config.return_values(path) elif op == 'exists': res = config.exists(path) + elif op == 'showConfig': + config_format = 'raw' + if 'configFormat' in command: + config_format = command['configFormat'] + + res = session.show_config(command['path'], format=config_format) else: return error(400, "\"{0}\" is not a valid operation".format(op)) except VyOSError as e: -- cgit v1.2.3 From 733ebc67ca80c13efa4fa809f6ba763a38c3d0f7 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 7 Aug 2019 10:21:08 +0200 Subject: [bridge] T1156: add missing if statement in config-migration Fixes: Traceback (most recent call last): File "/opt/vyatta/etc/config-migrate/migrate/interfaces/0-to-1", line 27, in for br in config.list_nodes(base): File "/usr/lib/python3/dist-packages/vyos/configtree.py", line 255, in list_nodes raise ConfigTreeError("Path [{}] doesn't exist".format(path_str)) vyos.configtree.ConfigTreeError: Path [b'interfaces bridge'] doesn't exist --- src/migration-scripts/interfaces/0-to-1 | 120 +++++++++++++++++--------------- 1 file changed, 62 insertions(+), 58 deletions(-) diff --git a/src/migration-scripts/interfaces/0-to-1 b/src/migration-scripts/interfaces/0-to-1 index b8e190f2c..38f2bd8f5 100755 --- a/src/migration-scripts/interfaces/0-to-1 +++ b/src/migration-scripts/interfaces/0-to-1 @@ -21,61 +21,65 @@ with open(file_name, 'r') as f: config = ConfigTree(config_file) base = ['interfaces', 'bridge'] -# -# make stp and igmp-snooping nodes valueless -# -for br in config.list_nodes(base): - # STP: check if enabled - stp_val = config.return_value(base + [br, 'stp']) - # STP: delete node with old syntax - config.delete(base + [br, 'stp']) - # STP: set new node - if enabled - if stp_val == "true": - config.set(base + [br, 'stp'], value=None) - - # igmp-snooping: check if enabled - igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: delete node with old syntax - config.delete(base + [br, 'igmp-snooping', 'querier']) - # igmp-snooping: set new node - if enabled - if igmp_val == "enable": - config.set(base + [br, 'igmp', 'querier'], value=None) - -# -# move interface based bridge-group to actual bridge (de-nest) -# -bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless'] -for type in bridge_types: - if not config.exists(['interfaces', type]): - continue - - for intf in config.list_nodes(['interfaces', type]): - # check if bridge-group exists - if config.exists(['interfaces', type, intf, 'bridge-group']): - bridge = config.return_value(['interfaces', type, intf, 'bridge-group', 'bridge']) - - # create new bridge member interface - config.set(base + [bridge, 'member', 'interface', intf]) - # format as tag node to avoid loading problems - config.set_tag(base + [bridge, 'member', 'interface']) - - # cost: migrate if configured - if config.exists(['interfaces', type, intf, 'bridge-group', 'cost']): - cost = config.return_value(['interfaces', type, intf, 'bridge-group', 'cost']) - # set new node - config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost) - - if config.exists(['interfaces', type, intf, 'bridge-group', 'priority']): - priority = config.return_value(['interfaces', type, intf, 'bridge-group', 'priority']) - # set new node - config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority) - - # Delete the old bridge-group assigned to an interface - config.delete(['interfaces', type, intf, 'bridge-group']) - - try: - with open(file_name, 'w') as f: - f.write(config.to_string()) - except OSError as e: - print("Failed to save the modified config: {}".format(e)) - sys.exit(1) +if not config.exists(base): + # Nothing to do + sys.exit(0) +else: + # + # make stp and igmp-snooping nodes valueless + # + for br in config.list_nodes(base): + # STP: check if enabled + stp_val = config.return_value(base + [br, 'stp']) + # STP: delete node with old syntax + config.delete(base + [br, 'stp']) + # STP: set new node - if enabled + if stp_val == "true": + config.set(base + [br, 'stp'], value=None) + + # igmp-snooping: check if enabled + igmp_val = config.return_value(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: delete node with old syntax + config.delete(base + [br, 'igmp-snooping', 'querier']) + # igmp-snooping: set new node - if enabled + if igmp_val == "enable": + config.set(base + [br, 'igmp', 'querier'], value=None) + + # + # move interface based bridge-group to actual bridge (de-nest) + # + bridge_types = ['bonding', 'ethernet', 'l2tpv3', 'openvpn', 'vxlan', 'wireless'] + for type in bridge_types: + if not config.exists(['interfaces', type]): + continue + + for intf in config.list_nodes(['interfaces', type]): + # check if bridge-group exists + if config.exists(['interfaces', type, intf, 'bridge-group']): + bridge = config.return_value(['interfaces', type, intf, 'bridge-group', 'bridge']) + + # create new bridge member interface + config.set(base + [bridge, 'member', 'interface', intf]) + # format as tag node to avoid loading problems + config.set_tag(base + [bridge, 'member', 'interface']) + + # cost: migrate if configured + if config.exists(['interfaces', type, intf, 'bridge-group', 'cost']): + cost = config.return_value(['interfaces', type, intf, 'bridge-group', 'cost']) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'cost'], value=cost) + + if config.exists(['interfaces', type, intf, 'bridge-group', 'priority']): + priority = config.return_value(['interfaces', type, intf, 'bridge-group', 'priority']) + # set new node + config.set(base + [bridge, 'member', 'interface', intf, 'priority'], value=priority) + + # Delete the old bridge-group assigned to an interface + config.delete(['interfaces', type, intf, 'bridge-group']) + + try: + with open(file_name, 'w') as f: + f.write(config.to_string()) + except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) -- cgit v1.2.3 From a7d5e9d23ab62829781c431d243f4b93c59b28a5 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 7 Aug 2019 10:23:50 +0200 Subject: [bridge] T1156: rename 'br_name' to 'intf' for indexing python dictionary interface name --- src/conf_mode/interface-bridge.py | 58 +++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/conf_mode/interface-bridge.py b/src/conf_mode/interface-bridge.py index 93eb3839c..543349e7b 100755 --- a/src/conf_mode/interface-bridge.py +++ b/src/conf_mode/interface-bridge.py @@ -30,7 +30,7 @@ default_config_data = { 'address': [], 'address_remove': [], 'aging': '300', - 'br_name': '', + 'arp_cache_timeout_ms': '30000', 'description': '', 'deleted': False, 'dhcp_client_id': '', @@ -42,7 +42,7 @@ default_config_data = { 'forwarding_delay': '15', 'hello_time': '2', 'igmp_querier': 0, - 'arp_cache_timeout_ms': '30000', + 'intf': '', 'mac' : '', 'max_age': '20', 'member': [], @@ -66,17 +66,17 @@ def get_config(): # determine tagNode instance try: - bridge['br_name'] = os.environ['VYOS_TAGNODE_VALUE'] + bridge['intf'] = os.environ['VYOS_TAGNODE_VALUE'] except KeyError as E: print("Interface not specified") # Check if bridge has been removed - if not conf.exists('interfaces bridge ' + bridge['br_name']): + if not conf.exists('interfaces bridge ' + bridge['intf']): bridge['deleted'] = True return bridge # set new configuration level - conf.set_level('interfaces bridge ' + bridge['br_name']) + conf.set_level('interfaces bridge ' + bridge['intf']) # retrieve configured interface addresses if conf.exists('address'): @@ -184,7 +184,7 @@ def verify(bridge): conf = Config() for br in conf.list_nodes('interfaces bridge'): # it makes no sense to verify ourself in this case - if br == bridge['br_name']: + if br == bridge['intf']: continue for intf in bridge['member']: @@ -207,91 +207,91 @@ def apply(bridge): cmd = '' if bridge['deleted']: # bridges need to be shutdown first - cmd += 'ip link set dev "{}" down'.format(bridge['br_name']) + cmd += 'ip link set dev "{}" down'.format(bridge['intf']) cmd += ' && ' # delete bridge - cmd += 'brctl delbr "{}"'.format(bridge['br_name']) + cmd += 'brctl delbr "{}"'.format(bridge['intf']) subprocess_cmd(cmd) else: # create bridge if it does not exist - if not os.path.exists("/sys/class/net/" + bridge['br_name']): + if not os.path.exists("/sys/class/net/" + bridge['intf']): # create bridge interface - cmd += 'brctl addbr "{}"'.format(bridge['br_name']) + cmd += 'brctl addbr "{}"'.format(bridge['intf']) cmd += ' && ' # activate "UP" the interface - cmd += 'ip link set dev "{}" up'.format(bridge['br_name']) + cmd += 'ip link set dev "{}" up'.format(bridge['intf']) cmd += ' && ' # set ageing time - cmd += 'brctl setageing "{}" "{}"'.format(bridge['br_name'], bridge['aging']) + cmd += 'brctl setageing "{}" "{}"'.format(bridge['intf'], bridge['aging']) cmd += ' && ' # set bridge forward delay - cmd += 'brctl setfd "{}" "{}"'.format(bridge['br_name'], bridge['forwarding_delay']) + cmd += 'brctl setfd "{}" "{}"'.format(bridge['intf'], bridge['forwarding_delay']) cmd += ' && ' # set hello time - cmd += 'brctl sethello "{}" "{}"'.format(bridge['br_name'], bridge['hello_time']) + cmd += 'brctl sethello "{}" "{}"'.format(bridge['intf'], bridge['hello_time']) cmd += ' && ' # set max message age - cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['br_name'], bridge['max_age']) + cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['intf'], bridge['max_age']) cmd += ' && ' # set bridge priority - cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['br_name'], bridge['priority']) + cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['intf'], bridge['priority']) cmd += ' && ' # turn stp on/off - cmd += 'brctl stp "{}" "{}"'.format(bridge['br_name'], bridge['stp']) + cmd += 'brctl stp "{}" "{}"'.format(bridge['intf'], bridge['stp']) for intf in bridge['member_remove']: # remove interface from bridge cmd += ' && ' - cmd += 'brctl delif "{}" "{}"'.format(bridge['br_name'], intf) + cmd += 'brctl delif "{}" "{}"'.format(bridge['intf'], intf) for intf in bridge['member']: # add interface to bridge # but only if it is not yet member of this bridge - if not os.path.exists('/sys/devices/virtual/net/' + bridge['br_name'] + '/brif/' + intf['name']): + if not os.path.exists('/sys/devices/virtual/net/' + bridge['intf'] + '/brif/' + intf['name']): cmd += ' && ' - cmd += 'brctl addif "{}" "{}"'.format(bridge['br_name'], intf['name']) + cmd += 'brctl addif "{}" "{}"'.format(bridge['intf'], intf['name']) # set bridge port cost if intf['cost']: cmd += ' && ' - cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['br_name'], intf['name'], intf['cost']) + cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['cost']) # set bridge port priority if intf['priority']: cmd += ' && ' - cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['br_name'], intf['name'], intf['priority']) + cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['intf'], intf['name'], intf['priority']) subprocess_cmd(cmd) # Change interface MAC address if bridge['mac']: - VyIfconfig.set_mac_address(bridge['br_name'], bridge['mac']) + VyIfconfig.set_mac_address(bridge['intf'], bridge['mac']) # update interface description used e.g. within SNMP - VyIfconfig.set_description(bridge['br_name'], bridge['description']) + VyIfconfig.set_description(bridge['intf'], bridge['description']) # Ignore link state changes? - VyIfconfig.set_link_detect(bridge['br_name'], bridge['disable_link_detect']) + VyIfconfig.set_link_detect(bridge['intf'], bridge['disable_link_detect']) # enable or disable IGMP querier - VyIfconfig.set_multicast_querier(bridge['br_name'], bridge['igmp_querier']) + VyIfconfig.set_multicast_querier(bridge['intf'], bridge['igmp_querier']) # ARP cache entry timeout in seconds - VyIfconfig.set_arp_cache_timeout(bridge['br_name'], bridge['arp_cache_timeout_ms']) + VyIfconfig.set_arp_cache_timeout(bridge['intf'], bridge['arp_cache_timeout_ms']) # Configure interface address(es) for addr in bridge['address_remove']: - VyIfconfig.remove_interface_address(bridge['br_name'], addr) + VyIfconfig.remove_interface_address(bridge['intf'], addr) for addr in bridge['address']: - VyIfconfig.add_interface_address(bridge['br_name'], addr) + VyIfconfig.add_interface_address(bridge['intf'], addr) return None -- cgit v1.2.3 From 5d9f2b33059bf314cb3e400139066e2024200c31 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 7 Aug 2019 11:56:37 +0200 Subject: Validator: rename cidr -> ip-cidr to match existing patterns --- interface-definitions/interfaces-bridge.xml | 2 +- interface-definitions/interfaces-wireguard.xml | 2 +- src/validators/cidr | 3 --- src/validators/ip-cidr | 3 +++ 4 files changed, 5 insertions(+), 5 deletions(-) delete mode 100755 src/validators/cidr create mode 100755 src/validators/ip-cidr diff --git a/interface-definitions/interfaces-bridge.xml b/interface-definitions/interfaces-bridge.xml index 93f374c80..d20582849 100644 --- a/interface-definitions/interfaces-bridge.xml +++ b/interface-definitions/interfaces-bridge.xml @@ -39,7 +39,7 @@ Dynamic Host Configuration Protocol for IPv6 - + (dhcp|dhcpv6) diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index c0102ea54..284a2e643 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -20,7 +20,7 @@ IP address - + ipv4net diff --git a/src/validators/cidr b/src/validators/cidr deleted file mode 100755 index 815aa8ba1..000000000 --- a/src/validators/cidr +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh - -ipaddrcheck --is-any-cidr $1 diff --git a/src/validators/ip-cidr b/src/validators/ip-cidr new file mode 100755 index 000000000..987bf84ca --- /dev/null +++ b/src/validators/ip-cidr @@ -0,0 +1,3 @@ +#!/bin/sh + +ipaddrcheck --is-any-cidr $1 -- cgit v1.2.3 From 89da1559e145d8e86750a634e5268df60e63b4b3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 7 Aug 2019 11:57:09 +0200 Subject: XML: WireGuard: run interfacedefinition through XML lint --- interface-definitions/interfaces-wireguard.xml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/interface-definitions/interfaces-wireguard.xml b/interface-definitions/interfaces-wireguard.xml index 284a2e643..6e2622018 100644 --- a/interface-definitions/interfaces-wireguard.xml +++ b/interface-definitions/interfaces-wireguard.xml @@ -5,7 +5,8 @@ WireGuard interface name - 459 + 459 + ^wg[0-9]{1,4} @@ -18,7 +19,7 @@ - IP address + IP address @@ -45,7 +46,7 @@ disables interface - + @@ -88,7 +89,7 @@ disables peer - + @@ -100,7 +101,7 @@ Key is not valid 44-character (32-bytes) base64 - + base64 encoded preshared key @@ -115,7 +116,7 @@ - + @@ -128,7 +129,7 @@ how often send keep alives in seconds - + -- cgit v1.2.3 From 097a088725eb632bec3e09a2e563fc96139d86ba Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 7 Aug 2019 11:35:26 +0200 Subject: Validator: add file-exists as replacement to Vyatta check_file_in_config_dir Verify if a file exists or not on the system. Can be called by: The --directory option is used to ensure a given file path lies under this (mandatory) directory. A directory can be mandatory when the optional argument -e, --error is used. This will return '1' instead of '0'. --- src/validators/file-exists | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100755 src/validators/file-exists diff --git a/src/validators/file-exists b/src/validators/file-exists new file mode 100755 index 000000000..e179805ed --- /dev/null +++ b/src/validators/file-exists @@ -0,0 +1,62 @@ +#!/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 . +# +# Description: +# Check if a given file exists on the system. Used for files that +# are referenced from the CLI and need to be preserved during an image upgrade. +# Warn the user if these aren't under /config + +import os +import sys +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument("-d", "--directory", type=str, help="File must be present in this directory.") +parser.add_argument("-e", "--error", action="store_true", help="Tread warnings as errors - change exit code to '1'") +parser.add_argument("file", type=str, help="Path of file to validate") + +args = parser.parse_args() + +msg_prefix = "WARNING: " +if args.error: + msg_prefix = "ERROR: " + +# +# Always check if the given file exists +# +if not os.path.exists(args.file): + print(msg_prefix + "File '{}' not found".format(args.file)) + if args.error: + sys.exit(1) + else: + sys.exit(0) + +# +# Optional check if the file is under a certain directory path +# +if args.directory: + # remove directory path from path to verify + rel_filename = args.file.replace(args.directory, '').lstrip('/') + + if not os.path.exists(args.directory + '/' + rel_filename): + print(msg_prefix + "'{}' lies outside of '{}' directory.\n" \ + "It will not get preserved during image upgrade!".format(args.file, args.directory)) + if args.error: + sys.exit(1) + else: + sys.exit(0) + +sys.exit(0) -- cgit v1.2.3 From f86d22ec54ee80cc54ae80edc0bbad8e419ba95c Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 6 Aug 2019 14:19:32 -0500 Subject: [service https] T1443: reset defaults on 'delete service https api' --- src/conf_mode/http-api.py | 13 +++++++------ src/conf_mode/https.py | 5 ++++- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py index 7d618dded..c1d596ea3 100755 --- a/src/conf_mode/http-api.py +++ b/src/conf_mode/http-api.py @@ -84,15 +84,16 @@ def generate(http_api): def apply(http_api): if http_api is not None: os.system('sudo systemctl restart vyos-http-api.service') - for dep in dependencies: - cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) - try: - subprocess.check_call(cmd, shell=True) - except subprocess.CalledProcessError as err: - raise ConfigError("{}.".format(err)) else: os.system('sudo systemctl stop vyos-http-api.service') + for dep in dependencies: + cmd = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) + try: + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + raise ConfigError("{}.".format(err)) + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index dae51dd7d..e1e81eef1 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -55,10 +55,13 @@ server { server_name {{ l_addr }}; {% endfor %} - location / { + # proxy settings for HTTP API, if enabled; 503, if not + location ~ /(retrieve|configure) { {% if api %} proxy_pass http://localhost:{{ api.port }}; proxy_buffering off; +{% else %} + return 503; {% endif %} } -- cgit v1.2.3 From b4a675c551523029ae89b61c841f54c1f2c02855 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Wed, 7 Aug 2019 20:26:45 +0000 Subject: [l2tp] T1566 ipv6 implementation --- interface-definitions/l2tp-server.xml | 40 +++++++++++++++++++++++++++ src/conf_mode/accel_l2tp.py | 52 ++++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/interface-definitions/l2tp-server.xml b/interface-definitions/l2tp-server.xml index d5b6a921b..721913dfe 100644 --- a/interface-definitions/l2tp-server.xml +++ b/interface-definitions/l2tp-server.xml @@ -67,6 +67,19 @@ + + + IPv6 Domain Name Service (DNS) server + + ipv6 + IPv6 DNS address + + + + + + + L2TP Network Server (LNS) @@ -255,6 +268,33 @@ + + + Pool of client IPv6 addresses + + + + + IPV6 prefix delegation + + ipv6prefix/mask,prefix_len + e.g.: fc00:0:1::/48,64 - divides prefix into /64 subnets for clients + + + + + + + DHCPv6 prefix delegation - rfc3633 + + ipv6prefix/mask,prefix_len + Delegate to clients through DHCPv6 prefix delegation - rfc3633 + + + + + + Description for L2TP remote-access settings diff --git a/src/conf_mode/accel_l2tp.py b/src/conf_mode/accel_l2tp.py index 3a224974e..3af8b7958 100755 --- a/src/conf_mode/accel_l2tp.py +++ b/src/conf_mode/accel_l2tp.py @@ -53,6 +53,9 @@ radius {% endif -%} ippool shaper +ipv6pool +ipv6_nd +ipv6_dhcp [core] thread-count={{thread_cnt}} @@ -72,6 +75,13 @@ dns2={{dns[1]}} {% endif %} {% endif -%} +{% if dnsv6 %} +[ipv6-dns] +{% for srv in dnsv6: %} +{{srv}} +{% endfor %} +{% endif %} + {% if wins %} [wins] {% if wins[0] %} @@ -127,6 +137,9 @@ lcp-echo-interval=30 {% if ccp_disable %} ccp=0 {% endif %} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %} {% if authentication['mode'] == 'radius' %} [radius] @@ -159,6 +172,21 @@ gw-ip-address={{outside_nexthop}} verbose=1 {% endif -%} +{% if client_ipv6_pool %} +[ipv6-pool] +{% for prfx in client_ipv6_pool.prefix: %} +{{prfx}} +{% endfor %} +{% for prfx in client_ipv6_pool.delegate_prefix: %} +delegate={{prfx}} +{% endfor %} +{% endif %} + +{% if client_ipv6_pool['delegate_prefix'] %} +[ipv6-dhcp] +verbose=1 +{% endif %} + {% if authentication['radiusopt']['shaper'] %} [shaper] verbose=1 @@ -170,6 +198,7 @@ vendor={{authentication['radiusopt']['shaper']['vendor']}} [cli] tcp=127.0.0.1:2004 +sessions-columns=ifname,username,calling-sid,ip,{{ip6_column}}{{ip6_dp_column}}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime ''' @@ -250,10 +279,14 @@ def get_config(): 'outside_addr' : '', 'outside_nexthop' : '', 'dns' : [], + 'dnsv6' : [], 'wins' : [], 'client_ip_pool' : None, 'client_ip_subnets' : [], - 'mtu' : '1436', + 'client_ipv6_pool' : {}, + 'mtu' : '1436', + 'ip6_column' : '', + 'ip6_dp_column' : '', } ### general options ### @@ -262,6 +295,9 @@ def get_config(): config_data['dns'].append( c.return_value('dns-servers server-1')) if c.exists('dns-servers server-2'): config_data['dns'].append( c.return_value('dns-servers server-2')) + if c.exists('dnsv6-servers'): + for dns6_server in c.return_values('dnsv6-servers'): + config_data['dnsv6'].append(dns6_server) if c.exists('wins-servers server-1'): config_data['wins'].append( c.return_value('wins-servers server-1')) if c.exists('wins-servers server-2'): @@ -369,6 +405,13 @@ def get_config(): if c.exists('client-ip-pool subnet'): config_data['client_ip_subnets'] = c.return_values('client-ip-pool subnet') + if c.exists('client-ipv6-pool prefix'): + config_data['client_ipv6_pool']['prefix'] = c.return_values('client-ipv6-pool prefix') + config_data['ip6_column'] = 'ip6,' + if c.exists('client-ipv6-pool delegate-prefix'): + config_data['client_ipv6_pool']['delegate_prefix'] = c.return_values('client-ipv6-pool delegate-prefix') + config_data['ip6_dp_column'] = 'ip6-dp,' + if c.exists('mtu'): config_data['mtu'] = c.return_value('mtu') @@ -424,6 +467,13 @@ def verify(c): #raise ConfigError('set vpn l2tp remote-access outside-nexthop required') print ("WARMING: set vpn l2tp remote-access outside-nexthop required") + ## check ipv6 + if 'delegate_prefix' in c['client_ipv6_pool'] and not 'prefix' in c['client_ipv6_pool']: + raise ConfigError("\"set vpn l2tp remote-access client-ipv6-pool prefix\" required for delegate-prefix ") + + if len(c['dnsv6']) > 3: + raise ConfigError("Maximum allowed dnsv6-servers addresses is 3") + def generate(c): if c == None: return None -- cgit v1.2.3 From d96cfc8a5b1e9f9a3484a4c4036dddabfc588f5b Mon Sep 17 00:00:00 2001 From: hagbard Date: Thu, 8 Aug 2019 13:19:02 -0700 Subject: [config] - T1557: Create generic abstraction for configuring interfaces e.g. IP address --- python/vyos/interfaceconfig.py | 357 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100755 python/vyos/interfaceconfig.py diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py new file mode 100755 index 000000000..7f9cc00f8 --- /dev/null +++ b/python/vyos/interfaceconfig.py @@ -0,0 +1,357 @@ +#!/usr/bin/python3 + +# Copyright 2019 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import sys +import os +import json +import socket +import subprocess + +dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' + +class Interface(): + def __init__(self, ifname=None, type=None): + if not ifname: + raise Exception("interface name required") + if not os.path.exists('/sys/class/net/{0}'.format(ifname)) and not type: + raise Exception("interface {0} not found".format(str(ifname))) + else: + if not os.path.exists('/sys/class/net/{0}'.format(ifname)): + try: + ret = subprocess.check_output(['ip link add dev ' + str(ifname) + ' type ' + type], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + if "Operation not supported" in str(e.output.decode()): + print(str(e.output.decode())) + sys.exit(0) + + self.ifname = str(ifname) + + def _debug(self, e=None): + """ + export DEBUG=1 to see debug messages + """ + if os.getenv('DEBUG') == '1': + if e: + print ("Exception raised:\ncommand: {0}\nerror code: {1}\nsubprocess output: {2}".format(e.cmd, e.returncode, e.output.decode()) ) + return True + return False + + + def set_alias(self, alias=None): + if not alias: + open('/sys/class/net/{0}/ifalias'.format(self.ifname),'w').write(self.ifname) + else: + open('/sys/class/net/{0}/ifalias'.format(self.ifname),'w').write(str(alias)) + + def get_alias(self): + return open('/sys/class/net/{0}/ifalias'.format(self.ifname),'r').read() + + def del_alias(self): + open('/sys/class/net/{0}/ifalias'.format(self.ifname),'w').write() + + + def set_link_state(self, state="up"): + if state.lower() == 'up' or state.lower() == 'down': + try: + ret = subprocess.check_output(['ip link set dev ' + self.ifname + ' ' + state], shell=True).decode() + return 0 + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return 1 + else: + raise ValueError("value can only be up or down") + + def get_link_state(self): + """ + returns either up/down or None if it can't find the state + """ + try: + ret = subprocess.check_output(['ip -j link show ' + self.ifname], shell=True).decode() + s = json.loads(ret) + return s[0]['operstate'].lower() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + + def remove_interface(self): + try: + ret = subprocess.check_output(['ip link del dev ' + self.ifname], shell=True).decode() + return 0 + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + + def set_macaddr(self, mac=None): + # ip will give us an error if the mac is invalid + try: + ret = subprocess.check_output(['ip link set address ' + mac + ' ' + self.ifname], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_macaddr(self): + try: + ret = subprocess.check_output(['ip -j -4 link show dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + return j[0]['address'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + + def get_ipv4_addr(self): + """ + reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface + """ + ips = [] + try: + ret = subprocess.check_output(['ip -j -4 addr show dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + + def get_ipv6_addr(self): + """ + reads all IPs assigned to an interface and returns it in a list, + or None if no IP address is assigned to the interface + """ + ips = [] + try: + ret = subprocess.check_output(['ip -j -6 addr show dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + for i in j: + if len(i) != 0: + for addr in i['addr_info']: + ips.append(addr['local']) + return ips + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + + def add_ipv4_addr(self, ipaddr=[]): + """ + add addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output(['ip -4 address add ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + + def del_ipv4_addr(self, ipaddr=[]): + """ + delete addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output(['ip -4 address del ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + + def add_ipv6_addr(self, ipaddr=[]): + """ + add addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output(['ip -6 address add ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + + def del_ipv6_addr(self, ipaddr=[]): + """ + delete addresses on the interface + """ + for ip in ipaddr: + try: + ret = subprocess.check_output(['ip -6 address del ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + return True + + + def get_mtu(self): + try: + ret = subprocess.check_output(['ip -j link list dev ' + self.ifname], shell=True).decode() + a = json.loads(ret)[0] + return a['mtu'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def set_mtu(self, mtu=None): + if not mtu: + return None + try: + ret = subprocess.check_output(['ip link set mtu ' + str(mtu) + ' dev ' + self.ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + + #### replace dhcpv4/v6 with systemd.networkd? + def set_dhcpv4(self): + conf_file = dhclient_conf_dir + self.ifname + '.conf' + pidfile = dhclient_conf_dir + self.ifname + '.pid' + leasefile = dhclient_conf_dir + self.ifname + '.leases' + + a = [ + '# generated by interface_config.py', + 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', + 'interface \"' + self.ifname + '\" {', + '\tsend host-name \"' + socket.gethostname() +'\";', + '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', + '}' + ] + + cnf = "" + for ln in a: + cnf +=str(ln + "\n") + open(conf_file, 'w').write(cnf) + if os.path.exists(dhclient_conf_dir + self.ifname + '.pid'): + try: + ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output(['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self.ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv4(self): + conf_file = dhclient_conf_dir + self.ifname + '.conf' + pidfile = dhclient_conf_dir + self.ifname + '.pid' + leasefile = dhclient_conf_dir + self.ifname + '.leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv4(self): + pidfile = dhclient_conf_dir + self.ifname + '.pid' + if not os.path.exists(pidfile): + print ("no dhcp client running on interface {0}".format(self.ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print("dhclient running on {0} with pid {1}".format(self.ifname, pid)) + return True + + + def set_dhcpv6(self): + conf_file = dhclient_conf_dir + self.ifname + '.v6conf' + pidfile = dhclient_conf_dir + self.ifname + '.v6pid' + leasefile = dhclient_conf_dir + self.ifname + '.v6leases' + a = [ + '# generated by interface_config.py', + 'interface \"' + self.ifname + '\" {', + '\trequest routers, domain-name-servers, domain-name;', + '}' + ] + cnf = "" + for ln in a: + cnf +=str(ln + "\n") + open(conf_file, 'w').write(cnf) + subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self.ifname + '.accept_ra=0']) + if os.path.exists(pidfile): + try: + ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + try: + ret = subprocess.check_output(['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self.ifname], shell=True).decode() + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def del_dhcpv6(self): + conf_file = dhclient_conf_dir + self.ifname + '.v6conf' + pidfile = dhclient_conf_dir + self.ifname + '.v6pid' + leasefile = dhclient_conf_dir + self.ifname + '.v6leases' + if not os.path.exists(pidfile): + return 1 + try: + ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() + subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self.ifname + '.accept_ra=1']) + return True + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None + + def get_dhcpv6(self): + pidfile = dhclient_conf_dir + self.ifname + '.v6pid' + if not os.path.exists(pidfile): + print ("no dhcpv6 client running on interface {0}".format(self.ifname)) + return False + else: + pid = open(pidfile, 'r').read() + print("dhclientv6 running on {0} with pid {1}".format(self.ifname, pid)) + return True + + +#### TODO: dhcpv6-pd via dhclient + -- cgit v1.2.3 From f7a7a936d4b1b2b0028fcfc1a1fcebfd069b239b Mon Sep 17 00:00:00 2001 From: zsdc Date: Fri, 9 Aug 2019 18:19:46 +0300 Subject: [bfd] T1183: Added some new functionality and fixed bugs in BFD: * added option "echo-mode" and "echo-interval" for BFD peers * added configuration check for usage "multihop" and "echo-mode" * added configuration check for denying deletion BFD peers, which are used in BGP configuration * fixed deleting/changing BFD peers with custom parameters (for example multihop, local-address, etc.) * deleted wrong skipping of configuration check for "shutdown" BFD peers --- interface-definitions/protocols-bfd.xml | 18 ++++ src/conf_mode/protocols_bfd.py | 179 +++++++++++++++++++++----------- 2 files changed, 138 insertions(+), 59 deletions(-) diff --git a/interface-definitions/protocols-bfd.xml b/interface-definitions/protocols-bfd.xml index 47d5bf97d..f2d7d7d2f 100644 --- a/interface-definitions/protocols-bfd.xml +++ b/interface-definitions/protocols-bfd.xml @@ -91,6 +91,18 @@ + + + Echo receive transmission interval + + 10-60000 + The minimal echo receive transmission interval that this system is capable of handling + + + + + + @@ -105,6 +117,12 @@ + + + Enables the echo transmission mode + + + diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index 04549f4b4..98f38035a 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -31,7 +31,7 @@ config_tmpl = """ ! bfd {% for peer in old_peers -%} - no peer {{ peer }} + no peer {{ peer.remote }}{% if peer.multihop %} multihop{% endif %}{% if peer.src_addr %} local-address {{ peer.src_addr }}{% endif %}{% if peer.src_if %} interface {{ peer.src_if }}{% endif %} {% endfor -%} ! {% for peer in new_peers -%} @@ -39,6 +39,8 @@ bfd detect-multiplier {{ peer.multiplier }} receive-interval {{ peer.rx_interval }} transmit-interval {{ peer.tx_interval }} + {% if peer.echo_mode %}echo-mode{% endif %} + {% if peer.echo_interval != '' %}echo-interval {{ peer.echo_interval }}{% endif %} {% if not peer.shutdown %}no {% endif %}shutdown {% endfor -%} ! @@ -49,6 +51,86 @@ default_config_data = { '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 = copy.deepcopy(default_config_data) conf = Config() @@ -60,56 +142,16 @@ def get_config(): # 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) - bfd['old_peers'] = conf.list_effective_nodes('peer') + 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'): - 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 - } - - # Check if individual peer is disabled - if conf.exists('shutdown'): - bfd_peer['shutdown'] = True - - # Check if peer has a local source interface configured - if 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.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.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.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.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.exists('interval multiplier'): - bfd_peer['multiplier'] = conf.return_value('interval multiplier') - - bfd['new_peers'].append(bfd_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 @@ -117,20 +159,39 @@ def verify(bfd): if bfd is None: return None - for peer in bfd['new_peers']: - # Bail out early if peer is shutdown - if peer['shutdown']: - continue + # some variables to use later + conf = Config() + for peer in bfd['new_peers']: # IPv6 peers require an explicit local address/interface combination if vyos.validate.is_ipv6(peer['remote']): if not (peer['src_if'] and peer['src_addr']): - raise ConfigError('BFD IPv6 peers require explicit local address/interface setting') - - # multihop doesn't accept interface names - if peer['multihop'] and peer['src_if']: - raise ConfigError('multihop does not accept interface names') - + raise ConfigError('BFD IPv6 peers require explicit local address and interface 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') + + # 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 -- cgit v1.2.3 From b570f31e7fce099687a12095850ebf9ae7a469ac Mon Sep 17 00:00:00 2001 From: hagbard Date: Fri, 9 Aug 2019 15:51:50 -0700 Subject: [config] - T1557: setting object properties for the class --- python/vyos/interfaceconfig.py | 217 ++++++++++++++++++++++------------------- 1 file changed, 118 insertions(+), 99 deletions(-) mode change 100755 => 100644 python/vyos/interfaceconfig.py diff --git a/python/vyos/interfaceconfig.py b/python/vyos/interfaceconfig.py old mode 100755 new mode 100644 index 7f9cc00f8..b8bfb707e --- a/python/vyos/interfaceconfig.py +++ b/python/vyos/interfaceconfig.py @@ -17,13 +17,14 @@ import sys import os +import re import json import socket import subprocess dhclient_conf_dir = r'/var/lib/dhcp/dhclient_' -class Interface(): +class Interface: def __init__(self, ifname=None, type=None): if not ifname: raise Exception("interface name required") @@ -40,7 +41,69 @@ class Interface(): print(str(e.output.decode())) sys.exit(0) - self.ifname = str(ifname) + self._ifname = str(ifname) + + + @property + def mtu(self): + return self._mtu + + @mtu.setter + def mtu(self, mtu=None): + if mtu < 68 or mtu > 9000: + raise ValueError("mtu size invalid value") + self._mtu = mtu + try: + ret = subprocess.check_output(['ip link set mtu ' + str(mtu) + ' dev ' + self._ifname], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + + + @property + def macaddr(self): + return self._macaddr + + @macaddr.setter + def macaddr(self, mac=None): + if not re.search('^[a-f0-9:]{17}$', str(mac)): + raise ValueError("mac address invalid") + self._macaddr = str(mac) + try: + ret = subprocess.check_output(['ip link set address ' + mac + ' ' + self._ifname], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + + @property + def ifalias(self): + return self._ifalias + + @ifalias.setter + def ifalias(self, ifalias=None): + if not ifalias: + self._ifalias = self._ifname + else: + self._ifalias = str(ifalias) + open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write(self._ifalias) + + @property + def linkstate(self): + return self._linkstate + + @linkstate.setter + def linkstate(self, state='up'): + if str(state).lower() == 'up' or str(state).lower() == 'down': + self._linkstate = str(state).lower() + else: + self._linkstate = 'up' + try: + ret = subprocess.check_output(['ip link set dev ' + self._ifname + ' ' + state], shell=True).decode() + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + + def _debug(self, e=None): """ @@ -52,38 +115,38 @@ class Interface(): return True return False + def get_mtu(self): + try: + ret = subprocess.check_output(['ip -j link list dev ' + self._ifname], shell=True).decode() + a = json.loads(ret)[0] + return a['mtu'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None - def set_alias(self, alias=None): - if not alias: - open('/sys/class/net/{0}/ifalias'.format(self.ifname),'w').write(self.ifname) - else: - open('/sys/class/net/{0}/ifalias'.format(self.ifname),'w').write(str(alias)) + def get_macaddr(self): + try: + ret = subprocess.check_output(['ip -j -4 link show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() + j = json.loads(ret) + return j[0]['address'] + except subprocess.CalledProcessError as e: + if self._debug(): + self._debug(e) + return None def get_alias(self): - return open('/sys/class/net/{0}/ifalias'.format(self.ifname),'r').read() + return open('/sys/class/net/{0}/ifalias'.format(self._ifname),'r').read() def del_alias(self): - open('/sys/class/net/{0}/ifalias'.format(self.ifname),'w').write() - - - def set_link_state(self, state="up"): - if state.lower() == 'up' or state.lower() == 'down': - try: - ret = subprocess.check_output(['ip link set dev ' + self.ifname + ' ' + state], shell=True).decode() - return 0 - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return 1 - else: - raise ValueError("value can only be up or down") + open('/sys/class/net/{0}/ifalias'.format(self._ifname),'w').write() def get_link_state(self): """ returns either up/down or None if it can't find the state """ try: - ret = subprocess.check_output(['ip -j link show ' + self.ifname], shell=True).decode() + ret = subprocess.check_output(['ip -j link show ' + self._ifname], shell=True).decode() s = json.loads(ret) return s[0]['operstate'].lower() except subprocess.CalledProcessError as e: @@ -91,37 +154,15 @@ class Interface(): self._debug(e) return None - def remove_interface(self): try: - ret = subprocess.check_output(['ip link del dev ' + self.ifname], shell=True).decode() + ret = subprocess.check_output(['ip link del dev ' + self._ifname], shell=True).decode() return 0 except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) return None - - def set_macaddr(self, mac=None): - # ip will give us an error if the mac is invalid - try: - ret = subprocess.check_output(['ip link set address ' + mac + ' ' + self.ifname], shell=True).decode() - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_macaddr(self): - try: - ret = subprocess.check_output(['ip -j -4 link show dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() - j = json.loads(ret) - return j[0]['address'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def get_ipv4_addr(self): """ reads all IPs assigned to an interface and returns it in a list, @@ -129,7 +170,7 @@ class Interface(): """ ips = [] try: - ret = subprocess.check_output(['ip -j -4 addr show dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output(['ip -j -4 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() j = json.loads(ret) for i in j: if len(i) != 0: @@ -149,7 +190,7 @@ class Interface(): """ ips = [] try: - ret = subprocess.check_output(['ip -j -6 addr show dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output(['ip -j -6 addr show dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() j = json.loads(ret) for i in j: if len(i) != 0: @@ -168,7 +209,7 @@ class Interface(): """ for ip in ipaddr: try: - ret = subprocess.check_output(['ip -4 address add ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output(['ip -4 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) @@ -182,7 +223,7 @@ class Interface(): """ for ip in ipaddr: try: - ret = subprocess.check_output(['ip -4 address del ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output(['ip -4 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) @@ -196,7 +237,7 @@ class Interface(): """ for ip in ipaddr: try: - ret = subprocess.check_output(['ip -6 address add ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output(['ip -6 address add ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) @@ -210,7 +251,7 @@ class Interface(): """ for ip in ipaddr: try: - ret = subprocess.check_output(['ip -6 address del ' + ip + ' dev ' + self.ifname], stderr=subprocess.STDOUT, shell=True).decode() + ret = subprocess.check_output(['ip -6 address del ' + ip + ' dev ' + self._ifname], stderr=subprocess.STDOUT, shell=True).decode() except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) @@ -218,38 +259,16 @@ class Interface(): return True - def get_mtu(self): - try: - ret = subprocess.check_output(['ip -j link list dev ' + self.ifname], shell=True).decode() - a = json.loads(ret)[0] - return a['mtu'] - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - def set_mtu(self, mtu=None): - if not mtu: - return None - try: - ret = subprocess.check_output(['ip link set mtu ' + str(mtu) + ' dev ' + self.ifname], shell=True).decode() - return True - except subprocess.CalledProcessError as e: - if self._debug(): - self._debug(e) - return None - - #### replace dhcpv4/v6 with systemd.networkd? def set_dhcpv4(self): - conf_file = dhclient_conf_dir + self.ifname + '.conf' - pidfile = dhclient_conf_dir + self.ifname + '.pid' - leasefile = dhclient_conf_dir + self.ifname + '.leases' + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' a = [ '# generated by interface_config.py', 'option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;', - 'interface \"' + self.ifname + '\" {', + 'interface \"' + self._ifname + '\" {', '\tsend host-name \"' + socket.gethostname() +'\";', '\trequest subnet-mask, broadcast-address, routers, domain-name-servers, rfc3442-classless-static-routes, domain-name, interface-mtu;', '}' @@ -259,14 +278,14 @@ class Interface(): for ln in a: cnf +=str(ln + "\n") open(conf_file, 'w').write(cnf) - if os.path.exists(dhclient_conf_dir + self.ifname + '.pid'): + if os.path.exists(dhclient_conf_dir + self._ifname + '.pid'): try: ret = subprocess.check_output(['/sbin/dhclient -4 -r -pf ' + pidfile], shell=True).decode() except subprocess.CalledProcessError as e: if self._debug(): self._debug(e) try: - ret = subprocess.check_output(['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self.ifname], shell=True).decode() + ret = subprocess.check_output(['/sbin/dhclient -4 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() return True except subprocess.CalledProcessError as e: if self._debug(): @@ -274,9 +293,9 @@ class Interface(): return None def del_dhcpv4(self): - conf_file = dhclient_conf_dir + self.ifname + '.conf' - pidfile = dhclient_conf_dir + self.ifname + '.pid' - leasefile = dhclient_conf_dir + self.ifname + '.leases' + conf_file = dhclient_conf_dir + self._ifname + '.conf' + pidfile = dhclient_conf_dir + self._ifname + '.pid' + leasefile = dhclient_conf_dir + self._ifname + '.leases' if not os.path.exists(pidfile): return 1 try: @@ -288,23 +307,23 @@ class Interface(): return None def get_dhcpv4(self): - pidfile = dhclient_conf_dir + self.ifname + '.pid' + pidfile = dhclient_conf_dir + self._ifname + '.pid' if not os.path.exists(pidfile): - print ("no dhcp client running on interface {0}".format(self.ifname)) + print ("no dhcp client running on interface {0}".format(self._ifname)) return False else: pid = open(pidfile, 'r').read() - print("dhclient running on {0} with pid {1}".format(self.ifname, pid)) + print("dhclient running on {0} with pid {1}".format(self._ifname, pid)) return True def set_dhcpv6(self): - conf_file = dhclient_conf_dir + self.ifname + '.v6conf' - pidfile = dhclient_conf_dir + self.ifname + '.v6pid' - leasefile = dhclient_conf_dir + self.ifname + '.v6leases' + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' a = [ '# generated by interface_config.py', - 'interface \"' + self.ifname + '\" {', + 'interface \"' + self._ifname + '\" {', '\trequest routers, domain-name-servers, domain-name;', '}' ] @@ -312,7 +331,7 @@ class Interface(): for ln in a: cnf +=str(ln + "\n") open(conf_file, 'w').write(cnf) - subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self.ifname + '.accept_ra=0']) + subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=0']) if os.path.exists(pidfile): try: ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() @@ -320,7 +339,7 @@ class Interface(): if self._debug(): self._debug(e) try: - ret = subprocess.check_output(['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self.ifname], shell=True).decode() + ret = subprocess.check_output(['/sbin/dhclient -6 -q -nw -cf ' + conf_file + ' -pf ' + pidfile + ' -lf ' + leasefile + ' ' + self._ifname], shell=True).decode() return True except subprocess.CalledProcessError as e: if self._debug(): @@ -328,14 +347,14 @@ class Interface(): return None def del_dhcpv6(self): - conf_file = dhclient_conf_dir + self.ifname + '.v6conf' - pidfile = dhclient_conf_dir + self.ifname + '.v6pid' - leasefile = dhclient_conf_dir + self.ifname + '.v6leases' + conf_file = dhclient_conf_dir + self._ifname + '.v6conf' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' + leasefile = dhclient_conf_dir + self._ifname + '.v6leases' if not os.path.exists(pidfile): return 1 try: ret = subprocess.check_output(['/sbin/dhclient -6 -q -x -pf ' + pidfile], shell=True).decode() - subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self.ifname + '.accept_ra=1']) + subprocess.call(['sysctl', '-q', '-w', 'net.ipv6.conf.' + self._ifname + '.accept_ra=1']) return True except subprocess.CalledProcessError as e: if self._debug(): @@ -343,13 +362,13 @@ class Interface(): return None def get_dhcpv6(self): - pidfile = dhclient_conf_dir + self.ifname + '.v6pid' + pidfile = dhclient_conf_dir + self._ifname + '.v6pid' if not os.path.exists(pidfile): - print ("no dhcpv6 client running on interface {0}".format(self.ifname)) + print ("no dhcpv6 client running on interface {0}".format(self._ifname)) return False else: pid = open(pidfile, 'r').read() - print("dhclientv6 running on {0} with pid {1}".format(self.ifname, pid)) + print("dhclientv6 running on {0} with pid {1}".format(self._ifname, pid)) return True -- cgit v1.2.3 From dc936192e87f114df6ab6a4df44967ce0f2b8390 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Sat, 10 Aug 2019 19:45:14 +0000 Subject: [snmp] T1575 Adding additional check for lspci --- src/op_mode/snmp_ifmib.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/op_mode/snmp_ifmib.py b/src/op_mode/snmp_ifmib.py index 9d56a950b..180892694 100755 --- a/src/op_mode/snmp_ifmib.py +++ b/src/op_mode/snmp_ifmib.py @@ -77,10 +77,16 @@ def show_ifdescr(i): proc = subprocess.Popen(['/usr/bin/lspci', '-mm', '-d', device], stdout=subprocess.PIPE) (out, err) = proc.communicate() + vendor = "" + device = "" + # convert output to string string = out.decode("utf-8").split('"') - vendor = string[3] - device = string[5] + if len(string) >= 3: + vendor = string[3] + + if len(string) >= 5: + device = string[5] ret = 'ifDescr = {0} {1}'.format(vendor, device) return ret.replace('\n', '') -- cgit v1.2.3 From 8cfe90afcaec171e03a266df7295d069ab260ef8 Mon Sep 17 00:00:00 2001 From: DmitriyEshenko Date: Tue, 13 Aug 2019 21:52:32 +0000 Subject: [bfd] T1183 Adding support show bfd counters --- op-mode-definitions/show-protocols-bfd.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/op-mode-definitions/show-protocols-bfd.xml b/op-mode-definitions/show-protocols-bfd.xml index 3c682d6f7..2a94d0497 100644 --- a/op-mode-definitions/show-protocols-bfd.xml +++ b/op-mode-definitions/show-protocols-bfd.xml @@ -11,6 +11,14 @@ Show all Bidirectional Forwarding Detection (BFD) peer status /usr/bin/vtysh -c "show bfd peers" + + + + Show Bidirectional Forwarding Detection (BFD) peer counters + + /usr/bin/vtysh -c "show bfd peers counters" + + @@ -20,6 +28,14 @@ /usr/bin/vtysh -c "show bfd peer $5" + + + + Show Bidirectional Forwarding Detection (BFD) peer counters + + /usr/bin/vtysh -c "show bfd peer $5 counters" + + -- cgit v1.2.3