diff options
-rw-r--r-- | data/templates/https/nginx.default.tmpl | 1 | ||||
-rw-r--r-- | data/templates/openvpn/server.conf.tmpl | 175 | ||||
-rw-r--r-- | interface-definitions/include/port-number.xml.i | 12 | ||||
-rw-r--r-- | interface-definitions/include/vif-s.xml.i | 1 | ||||
-rw-r--r-- | interface-definitions/interfaces-tunnel.xml.in | 2 | ||||
-rw-r--r-- | interface-definitions/interfaces-wireguard.xml.in | 36 | ||||
-rw-r--r-- | python/vyos/remote.py | 29 | ||||
-rw-r--r-- | python/vyos/util.py | 15 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-openvpn.py | 1 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-pppoe.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-tunnel.py | 151 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireguard.py | 411 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wireless.py | 8 | ||||
-rwxr-xr-x | src/conf_mode/interfaces-wirelessmodem.py | 4 | ||||
-rwxr-xr-x | src/conf_mode/vrf.py | 1 | ||||
-rwxr-xr-x | src/helpers/vyos-merge-config.py | 10 | ||||
-rwxr-xr-x | src/op_mode/dns_forwarding_reset.py | 17 | ||||
-rwxr-xr-x | src/op_mode/lldp_op.py | 6 | ||||
-rwxr-xr-x | src/op_mode/wireguard.py | 11 |
19 files changed, 536 insertions, 359 deletions
diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index 33f7b2820..f4f2c1848 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -43,6 +43,7 @@ server { location ~ /(retrieve|configure|config-file|image|generate|show) { {% if server.api %} proxy_pass http://localhost:{{ server.api.port }}; + proxy_read_timeout 600; proxy_buffering off; {% else %} return 503; diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index e7715dfb5..5f6d1fc3c 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -3,18 +3,20 @@ # See https://community.openvpn.net/openvpn/wiki/Openvpn24ManPage # for individual keyword definition -{% if description %} +{% if description -%} # {{ description }} -{% endif %} + +{% endif -%} verb 3 status /opt/vyatta/etc/openvpn/status/{{ intf }}.status 30 writepid /var/run/openvpn/{{ intf }}.pid -dev-type {{ type }} -dev {{ intf }} user {{ uid }} group {{ gid }} + +dev-type {{ type }} +dev {{ intf }} persist-key iproute /usr/libexec/vyos/system/unpriv-ip @@ -22,187 +24,188 @@ proto {% if 'tcp-active' in protocol -%}tcp-client{% elif 'tcp-passive' in proto {%- if local_host %} local {{ local_host }} -{% endif %} +{%- endif %} {%- if mode == 'server' and protocol == 'udp' and not local_host %} multihome -{% endif %} +{%- endif %} {%- if local_port %} lport {{ local_port }} -{% endif %} +{%- endif %} -{%- if remote_port %} +{% if remote_port -%} rport {{ remote_port }} {% endif %} {%- if remote_host %} -{% for remote in remote_host -%} +{%- for remote in remote_host -%} remote {{ remote }} {% endfor -%} -{% endif %} +{% endif -%} -{%- if shared_secret_file %} +{% if shared_secret_file %} secret {{ shared_secret_file }} -{% endif %} +{%- endif %} {%- if persistent_tunnel %} persist-tun -{% endif %} +{%- endif %} + +{%- if redirect_gateway %} +push "redirect-gateway {{ redirect_gateway }}" +{%- endif %} -{%- if mode %} -{%- if 'client' in mode %} +{%- if compress_lzo %} +compress lzo +{%- endif %} + +{% if 'client' in mode -%} # # OpenVPN Client mode # client nobind -{%- elif 'server' in mode %} + +{% elif 'server' in mode -%} # # OpenVPN Server mode # -mode server -tls-server -keepalive {{ ping_interval }} {{ ping_restart }} -management /tmp/openvpn-mgmt-intf unix {%- if server_topology %} topology {% if 'point-to-point' in server_topology %}p2p{% else %}subnet{% endif %} -{% endif %} - -{% for ns in server_dns_nameserver -%} -push "dhcp-option DNS {{ ns }}" -{% endfor -%} - -{% for route in server_push_route -%} -push "route {{ route }}" -{% endfor -%} - -{%- if server_domain %} -push "dhcp-option DOMAIN {{ server_domain }}" -{% endif %} - -{%- if server_max_conn %} -max-clients {{ server_max_conn }} -{% endif %} +{%- endif %} {%- if bridge_member %} server-bridge nogw {%- else %} server {{ server_subnet }} -{% endif %} +{%- endif %} + +{%- if server_max_conn %} +max-clients {{ server_max_conn }} +{%- endif %} {%- if server_reject_unconfigured %} ccd-exclusive +{%- endif %} + +keepalive {{ ping_interval }} {{ ping_restart }} +management /tmp/openvpn-mgmt-intf unix + +{% for route in server_push_route -%} +push "route {{ route }}" +{% endfor -%} + +{% for ns in server_dns_nameserver -%} +push "dhcp-option DNS {{ ns }}" +{% endfor -%} + +{%- if server_domain -%} +push "dhcp-option DOMAIN {{ server_domain }}" {% endif %} -{%- else %} +{% else -%} # # OpenVPN site-2-site mode # ping {{ ping_interval }} ping-restart {{ ping_restart }} -{%- if local_address_subnet %} +{% if local_address_subnet -%} ifconfig {{ local_address }} {{ local_address_subnet }} -{% elif remote_address %} +{%- elif remote_address -%} ifconfig {{ local_address }} {{ remote_address }} -{% endif %} +{%- endif %} -{% endif %} -{% endif %} +{% endif -%} +{% if tls -%} +# TLS options {%- if tls_ca_cert %} ca {{ tls_ca_cert }} -{% endif %} +{%- endif %} {%- if tls_cert %} cert {{ tls_cert }} -{% endif %} +{%- endif %} {%- if tls_key %} key {{ tls_key }} -{% endif %} +{%- endif %} {%- if tls_crypt %} tls-crypt {{ tls_crypt }} -{% endif %} +{%- endif %} {%- if tls_crl %} crl-verify {{ tls_crl }} -{% endif %} +{%- endif %} {%- if tls_version_min %} tls-version-min {{tls_version_min}} -{% endif %} +{%- endif %} {%- if tls_dh %} dh {{ tls_dh }} -{% endif %} +{%- endif %} {%- if tls_auth %} tls-auth {{tls_auth}} -{% endif %} +{%- endif %} +{%- if tls_role %} {%- if 'active' in tls_role %} tls-client {%- elif 'passive' in tls_role %} tls-server -{% endif %} +{%- endif %} +{%- endif %} -{%- if redirect_gateway %} -push "redirect-gateway {{ redirect_gateway }}" -{% endif %} - -{%- if compress_lzo %} -compress lzo -{% endif %} - -{%- if hash %} -auth {{ hash }} -{% endif %} +{%- endif %} +# Encryption options {%- if encryption %} -{%- if 'des' in encryption %} +{% if encryption == 'des' -%} cipher des-cbc -{%- elif '3des' in encryption %} +{%- elif encryption == '3des' -%} cipher des-ede3-cbc -{%- elif 'bf128' in encryption %} +{%- elif encryption == 'bf128' -%} cipher bf-cbc keysize 128 -{%- elif 'bf256' in encryption %} +{%- elif encryption == 'bf256' -%} cipher bf-cbc keysize 25 -{%- elif 'aes128gcm' in encryption %} +{%- elif encryption == 'aes128gcm' -%} cipher aes-128-gcm -{%- elif 'aes128' in encryption %} +{%- elif encryption == 'aes128' -%} cipher aes-128-cbc -{%- elif 'aes192gcm' in encryption %} +{%- elif encryption == 'aes192gcm' -%} cipher aes-192-gcm -{%- elif 'aes192' in encryption %} +{%- elif encryption == 'aes192' -%} cipher aes-192-cbc -{%- elif 'aes256gcm' in encryption %} +{%- elif encryption == 'aes256gcm' -%} cipher aes-256-gcm -{%- elif 'aes256' in encryption %} +{%- elif encryption == 'aes256' -%} cipher aes-256-cbc -{% endif %} -{% endif %} +{%- endif -%} +{%- endif %} {%- if ncp_ciphers %} ncp-ciphers {{ncp_ciphers}} -{% endif %} +{%- endif %} {%- if disable_ncp %} ncp-disable -{% endif %} +{%- endif %} + +{% if hash -%} +auth {{ hash }} +{%- endif -%} {%- if auth %} auth-user-pass /tmp/openvpn-{{ intf }}-pw auth-retry nointeract -{% endif %} - -{%- if client %} -client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} -{% endif %} +{%- endif %} # DEPRECATED This option will be removed in OpenVPN 2.5 # Until OpenVPN v2.3 the format of the X.509 Subject fields was formatted like this: @@ -218,6 +221,12 @@ client-config-dir /opt/vyatta/etc/openvpn/ccd/{{ intf }} # See https://phabricator.vyos.net/T1512 compat-names +{% if options -%} +# +# Custom options added by user (not validated) +# + {% for option in options -%} {{ option }} {% endfor -%} +{%- endif %} diff --git a/interface-definitions/include/port-number.xml.i b/interface-definitions/include/port-number.xml.i new file mode 100644 index 000000000..78eb4b7af --- /dev/null +++ b/interface-definitions/include/port-number.xml.i @@ -0,0 +1,12 @@ +<leafNode name="port">
+ <properties>
+ <help>Port number used to establish connection</help>
+ <valueHelp>
+ <format>1-65535</format>
+ <description>Numeric IP port</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ </properties>
+</leafNode>
diff --git a/interface-definitions/include/vif-s.xml.i b/interface-definitions/include/vif-s.xml.i index 2120aa32d..acbb34dbd 100644 --- a/interface-definitions/include/vif-s.xml.i +++ b/interface-definitions/include/vif-s.xml.i @@ -12,6 +12,7 @@ #include <include/dhcp-dhcpv6-options.xml.i> #include <include/interface-disable-link-detect.xml.i> #include <include/interface-disable.xml.i> + #include <include/interface-vrf.xml.i> <leafNode name="ethertype"> <properties> <help>Set Ethertype</help> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index 3ba82067f..e1ac60319 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -107,7 +107,7 @@ </leafNode> <leafNode name="encapsulation"> <properties> - <help>Ignore link state changes</help> + <help>Encapsulation of this tunnel interface</help> <completionHelp> <list>gre gre-bridge ipip sit ipip6 ip6ip6 ip6gre</list> </completionHelp> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index d3f084774..9db608afb 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -19,26 +19,9 @@ #include <include/address-ipv4-ipv6.xml.i> #include <include/interface-description.xml.i> #include <include/interface-disable.xml.i> - <leafNode name="port"> - <properties> - <help>Local port to listen for incoming connections</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> - <leafNode name="mtu"> - <properties> - <help>interface mtu size(default: 1420)</help> - <constraint> - <validator name="numeric" argument="--range 68-9000"/> - </constraint> - </properties> - </leafNode> + #include <include/interface-vrf.xml.i> + #include <include/port-number.xml.i> + #include <include/interface-mtu-68-9000.xml.i> <leafNode name="fwmark"> <properties> <help>A 32-bit fwmark value set on all outgoing packets</help> @@ -113,18 +96,7 @@ </constraint> </properties> </leafNode> - <leafNode name="port"> - <properties> - <help>Port number on tunnel remote end</help> - <valueHelp> - <format>1-65535</format> - <description>Numeric IP port</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-65535"/> - </constraint> - </properties> - </leafNode> + #include <include/port-number.xml.i> <leafNode name="persistent-keepalive"> <properties> <help>how often send keep alives in seconds</help> diff --git a/python/vyos/remote.py b/python/vyos/remote.py index f0bf41cd4..f8a21f068 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -17,8 +17,7 @@ import sys import os import re import fileinput - -from vyos.util import cmd, DEVNULL +import subprocess def check_and_add_host_key(host_name): @@ -34,8 +33,10 @@ def check_and_add_host_key(host_name): keyscan_cmd = 'ssh-keyscan -t rsa {} 2>/dev/null'.format(host_name) try: - host_key = cmd(keyscan_cmd, stderr=DEVNULL, universal_newlines=True) - except OSError: + host_key = subprocess.check_output(keyscan_cmd, shell=True, + stderr=subprocess.DEVNULL, + universal_newlines=True) + except subprocess.CalledProcessError as err: sys.exit("Can not get RSA host key") # libssh2 (jessie; stretch) does not recognize ec host keys, and curl @@ -63,8 +64,10 @@ def check_and_add_host_key(host_name): fingerprint_cmd = 'ssh-keygen -lf /dev/stdin <<< "{}"'.format(host_key) try: - fingerprint = cmd(fingerprint_cmd, stderr=DEVNULL, universal_newlines=True) - except OSError: + fingerprint = subprocess.check_output(fingerprint_cmd, shell=True, + stderr=subprocess.DEVNULL, + universal_newlines=True) + except subprocess.CalledProcessError as err: sys.exit("Can not get RSA host key fingerprint.") print("RSA host key fingerprint is {}".format(fingerprint.split()[1])) @@ -125,8 +128,9 @@ def get_remote_config(remote_file): # Try header first, and look for 'OK' or 'Moved' codes: curl_cmd = 'curl {0} -q -I {1}'.format(redirect_opt, remote_file) try: - curl_output = cmd(curl_cmd, shell=True, universal_newlines=True) - except OSError: + curl_output = subprocess.check_output(curl_cmd, shell=True, + universal_newlines=True) + except subprocess.CalledProcessError: sys.exit(1) return_vals = re.findall(r'^HTTP\/\d+\.?\d\s+(\d+)\s+(.*)$', @@ -142,6 +146,9 @@ def get_remote_config(remote_file): curl_cmd = 'curl {0} -# {1}'.format(redirect_opt, remote_file) try: - return cmd(curl_cmd, universal_newlines=True) - except OSError: - return None + config_file = subprocess.check_output(curl_cmd, shell=True, + universal_newlines=True) + except subprocess.CalledProcessError: + config_file = None + + return config_file diff --git a/python/vyos/util.py b/python/vyos/util.py index e61de52bd..c827425ee 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -99,16 +99,25 @@ def read_file(path): return data -def chown_file(path, user, group): - """ change file owner """ +def chown(path, user, group): + """ change file/directory owner """ from pwd import getpwnam from grp import getgrnam - if os.path.isfile(path): + if os.path.exists(path): uid = getpwnam(user).pw_uid gid = getgrnam(group).gr_gid os.chown(path, uid, gid) +def chmod_750(path): + """ make file/directory only executable to user and group """ + from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP + + if os.path.exists(path): + bitmask = S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | S_IXGRP + os.chmod(path, bitmask) + + def chmod_x(path): """ make file executable """ from stat import S_IRUSR, S_IWUSR, S_IXUSR, S_IRGRP, S_IXGRP, S_IROTH, S_IXOTH diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index e9b40bb38..f34e4f7fe 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -428,6 +428,7 @@ def get_config(): # Minimum required TLS version if conf.exists('tls tls-version-min'): openvpn['tls_version_min'] = conf.return_value('tls tls-version-min') + openvpn['tls'] = True if conf.exists('shared-secret-key-file'): openvpn['shared_secret_file'] = conf.return_value('shared-secret-key-file') diff --git a/src/conf_mode/interfaces-pppoe.py b/src/conf_mode/interfaces-pppoe.py index 407547175..26441838e 100755 --- a/src/conf_mode/interfaces-pppoe.py +++ b/src/conf_mode/interfaces-pppoe.py @@ -24,7 +24,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import Interface -from vyos.util import chown_file, chmod_x, cmd +from vyos.util import chown, chmod_x, cmd from vyos import ConfigError default_config_data = { @@ -240,7 +240,7 @@ def apply(pppoe): cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - chown_file(pppoe['logfile'], 'root', 'vyattacfg') + chown(pppoe['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/interfaces-tunnel.py b/src/conf_mode/interfaces-tunnel.py index 646e61c53..28b1cf60f 100755 --- a/src/conf_mode/interfaces-tunnel.py +++ b/src/conf_mode/interfaces-tunnel.py @@ -29,18 +29,99 @@ from vyos import ConfigError class FixedDict(dict): + """ + FixedDict: A dictionnary not allowing new keys to be created after initialisation. + + >>> f = FixedDict(**{'count':1}) + >>> f['count'] = 2 + >>> f['king'] = 3 + File "...", line ..., in __setitem__ + raise ConfigError(f'Option "{k}" has no defined default') + """ def __init__ (self, **options): self._allowed = options.keys() super().__init__(**options) def __setitem__ (self, k, v): + """ + __setitem__ is a builtin which is called by python when setting dict values: + >>> d = dict() + >>> d['key'] = 'value' + >>> d + {'key': 'value'} + + is syntaxic sugar for + + >>> d = dict() + >>> d.__setitem__('key','value') + >>> d + {'key': 'value'} + """ if k not in self._allowed: raise ConfigError(f'Option "{k}" has no defined default') super().__setitem__(k, v) -class ConfigurationState (Config): +class ConfigurationState(Config): + """ + The current API require a dict to be generated by get_config() + which is then consumed by verify(), generate() and apply() + + ConfiguartionState is an helper class wrapping Config and providing + an common API to this dictionary structure + + Its to_dict() function return a dictionary containing three fields, + each a dict, called options, changes, actions. + + options: + + contains the configuration options for the dict and its value + {'options': {'commment': 'test'}} will be set if + 'set interface dummy dum1 description test' was used and + the key 'commment' is used to index the description info. + + changes: + + per key, let us know how the data was modified using one of the action + a special key called 'section' is used to indicate what happened to the + section. for example: + + 'set interface dummy dum1 description test' when no interface was setup + will result in the following changes + {'changes': {'section': 'create', 'comment': 'create'}} + + on an existing interface, depending if there was a description + 'set interface dummy dum1 description test' will result in one of + {'changes': {'comment': 'create'}} (not present before) + {'changes': {'comment': 'static'}} (unchanged) + {'changes': {'comment': 'modify'}} (changed from half) + + and 'delete interface dummy dummy1 description' will result in: + {'changes': {'comment': 'delete'}} + + actions: + + for each action list the configuration key which were changes + in our example if we added the 'description' and added an IP we would have + {'actions': { 'create': ['comment'], 'modify': ['addresses-add']}} + + the actions are: + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + which for each field represent how it was modified since the last commit + """ + def __init__ (self, section, default): + """ + initialise the class for a given configuration path: + + >>> conf = ConfigurationState('interfaces ethernet eth1') + all further references to get_value(s) and get_effective(s) + will be for this part of the configuration (eth1) + """ super().__init__() self.section = section self.default = deepcopy(default) @@ -61,6 +142,15 @@ class ConfigurationState (Config): self.changes['section'] = 'create' def _act(self, section): + """ + Returns for a given configuration field determine what happened to it + + 'create': it did not exist previously and was created + 'modify': it did exist previously but its content changed + 'static': it did exist and did not change + 'delete': it was present but was removed from the configuration + 'absent': it was not and is not present + """ if self.exists(section): if self.exists_effective(section): if self.return_value(section) != self.return_effective_value(section): @@ -89,24 +179,71 @@ class ConfigurationState (Config): self.options[name] = value def get_value(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_value + (the data in the configuration to apply) + """ if self._action(name, key) in ('delete', 'absent'): return return self._get(name, key, default, self.return_value) def get_values(self, name, key, default=None): + """ + >>> conf.get_values('addresses-add', 'address') + will place a list made of the IP present in 'interface dummy dum1 address' + into the dictionnary entry 'addr' using Config.return_values + (the data in the configuration to apply) + """ if self._action(name, key) in ('delete', 'absent'): return return self._get(name, key, default, self.return_values) def get_effective(self, name, key, default=None): + """ + >>> conf.get_value('comment', 'description') + will place the string of 'interface dummy description test' + into the dictionnary entry 'comment' using Config.return_effective_value + (the data in the configuration to apply) + """ self._action(name, key) return self._get(name, key, default, self.return_effective_value) def get_effectives(self, name, key, default=None): + """ + >>> conf.get_effectives('addresses-add', 'address') + will place a list made of the IP present in 'interface ethernet eth1 address' + into the dictionnary entry 'addresses-add' using Config.return_effectives_value + (the data in the un-modified configuration) + """ self._action(name, key) return self._get(name, key, default, self.return_effectives_value) def load(self, mapping): + """ + load will take a dictionary defining how we wish the configuration + to be parsed and apply this definition to set the data. + + >>> mapping = { + 'addresses-add' : ('address', True, None), + 'comment' : ('description', False, 'auto'), + } + >>> conf.load(mapping) + + mapping is a dictionary where each key represents the name we wish + to have (such as 'addresses-add'), with a list a content representing + how the data should be parsed: + - the configuration section name + such as 'address' under 'interface ethernet eth1' + - boolean indicating if this data can have multiple values + for 'address', True, as multiple IPs can be set + for 'description', False, as it is a single string + - default represent the default value if absent from the configuration + 'None' indicate that no default should be set if the configuration + does not have the configuration section + + """ for local_name, (config_name, multiple, default) in mapping.items(): if multiple: self.get_values(local_name, config_name, default) @@ -114,12 +251,21 @@ class ConfigurationState (Config): self.get_value(local_name, config_name, default) def remove_default (self,*options): + """ + remove all the values which were not changed from the default + """ for option in options: if self.exists(option) and self_return_value(option) != self.default[option]: continue del self.options[option] def to_dict (self): + """ + provide a dictionary with the generated data for the configuration + options: the configuration value for the key + changes: per key how they changed from the previous configuration + actions: per changes all the options which were changed + """ # as we have to use a dict() for the API for verify and apply the options return { 'options': self.options, @@ -203,7 +349,8 @@ def get_class (options): } kls = dispatch[options['type']] - if options['type'] == 'gre' and not options['remote']: + if options['type'] == 'gre' and not options['remote'] \ + and not options['key'] and not options['multicast']: # will use GreTapIf on GreIf deletion but it does not matter return GRETapIf elif options['type'] == 'sit' and options['6rd-prefix']: diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 54121a6c1..8e80a85a2 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -14,173 +14,180 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import sys import os import re +from sys import exit from copy import deepcopy from netifaces import interfaces -from vyos import ConfigError from vyos.config import Config from vyos.configdict import list_diff -from vyos.util import run, is_bridge_member from vyos.ifconfig import WireGuardIf +from vyos.util import chown, run, is_bridge_member, chmod_750 +from vyos import ConfigError kdir = r'/config/auth/wireguard' +default_config_data = { + 'intfc': '', + 'address': [], + 'address_remove': [], + 'description': '', + 'lport': None, + 'deleted': False, + 'disable': False, + 'fwmark': 0x00, + 'mtu': 1420, + 'peer': [], + 'peer_remove': [], # stores public keys of peers to remove + 'pk': f'{kdir}/default/private.key', + 'vrf': '' +} + def _check_kmod(): - if not os.path.exists('/sys/module/wireguard'): - if run('modprobe wireguard') != 0: - raise ConfigError("modprobe wireguard failed") + modules = ['wireguard'] + for module in modules: + if not os.path.exists(f'/sys/module/{module}'): + if run(f'modprobe {module}') != 0: + raise ConfigError(f'Loading Kernel module {module} failed') def _migrate_default_keys(): if os.path.exists(f'{kdir}/private.key') and not os.path.exists(f'{kdir}/default/private.key'): - old_umask = os.umask(0o027) location = f'{kdir}/default' - run(f'sudo mkdir -p {location}') - run(f'sudo chgrp vyattacfg {location}') - run(f'sudo chmod 750 {location}') + if not os.path.exists(location): + os.makedirs(location) + + chown(location, 'root', 'vyattacfg') + chmod_750(location) os.rename(f'{kdir}/private.key', f'{location}/private.key') os.rename(f'{kdir}/public.key', f'{location}/public.key') - os.umask(old_umask) def get_config(): - c = Config() - if not c.exists(['interfaces', 'wireguard']): - return None + conf = Config() + base = ['interfaces', 'wireguard'] # determine tagNode instance if 'VYOS_TAGNODE_VALUE' not in os.environ: raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified') - dflt_cnf = { - 'intfc': '', - 'addr': [], - 'addr_remove': [], - 'descr': '', - 'lport': None, - 'delete': False, - 'state': 'up', - 'fwmark': 0x00, - 'mtu': 1420, - 'peer': {}, - 'peer_remove': [], - 'pk': '{}/default/private.key'.format(kdir) - } - - ifname = str(os.environ['VYOS_TAGNODE_VALUE']) - wg = deepcopy(dflt_cnf) - wg['intfc'] = ifname - wg['descr'] = ifname - - c.set_level(['interfaces', 'wireguard']) - - # interface removal state - if not c.exists(ifname) and c.exists_effective(ifname): - wg['delete'] = True - - if not wg['delete']: - c.set_level(['interfaces', 'wireguard', ifname]) - if c.exists(['address']): - wg['addr'] = c.return_values(['address']) - - # determine addresses which need to be removed - eff_addr = c.return_effective_values(['address']) - wg['addr_remove'] = list_diff(eff_addr, wg['addr']) - - # ifalias description - if c.exists(['description']): - wg['descr'] = c.return_value(['description']) - - # link state - if c.exists(['disable']): - wg['state'] = 'down' - - # local port to listen on - if c.exists(['port']): - wg['lport'] = c.return_value(['port']) - - # fwmark value - if c.exists(['fwmark']): - wg['fwmark'] = c.return_value(['fwmark']) - - # mtu - if c.exists('mtu'): - wg['mtu'] = c.return_value('mtu') - - # private key - if c.exists(['private-key']): - wg['pk'] = "{0}/{1}/private.key".format( - kdir, c.return_value(['private-key'])) - - # peer removal, wg identifies peers by its pubkey - peer_eff = c.list_effective_nodes(['peer']) - peer_rem = list_diff(peer_eff, c.list_nodes(['peer'])) - for p in peer_rem: - wg['peer_remove'].append( - c.return_effective_value(['peer', p, 'pubkey'])) - - # peer settings - if c.exists(['peer']): - for p in c.list_nodes(['peer']): - if not c.exists(['peer', p, 'disable']): - wg['peer'].update( - { - p: { - 'allowed-ips': [], - 'address': '', - 'port': '', - 'pubkey': '' - } - } - ) - # peer allowed-ips - if c.exists(['peer', p, 'allowed-ips']): - wg['peer'][p]['allowed-ips'] = c.return_values( - ['peer', p, 'allowed-ips']) - # peer address - if c.exists(['peer', p, 'address']): - wg['peer'][p]['address'] = c.return_value( - ['peer', p, 'address']) - # peer port - if c.exists(['peer', p, 'port']): - wg['peer'][p]['port'] = c.return_value( - ['peer', p, 'port']) - # persistent-keepalive - if c.exists(['peer', p, 'persistent-keepalive']): - wg['peer'][p]['persistent-keepalive'] = c.return_value( - ['peer', p, 'persistent-keepalive']) - # preshared-key - if c.exists(['peer', p, 'preshared-key']): - wg['peer'][p]['psk'] = c.return_value( - ['peer', p, 'preshared-key']) - # peer pubkeys - key_eff = c.return_effective_value(['peer', p, 'pubkey']) - key_cfg = c.return_value(['peer', p, 'pubkey']) - wg['peer'][p]['pubkey'] = key_cfg - - # on a pubkey change we need to remove the pubkey first - # peers are identified by pubkey, so key update means - # peer removal and re-add - if key_eff != key_cfg and key_eff != None: - wg['peer_remove'].append(key_cfg) - - # if a peer is disabled, we have to exec a remove for it's pubkey - else: - peer_key = c.return_value(['peer', p, 'pubkey']) - wg['peer_remove'].append(peer_key) + wg = deepcopy(default_config_data) + wg['intf'] = os.environ['VYOS_TAGNODE_VALUE'] + + # Check if interface has been removed + if not conf.exists(base + [wg['intf']]): + wg['deleted'] = True + return wg + + conf.set_level(base + [wg['intf']]) + + # retrieve configured interface addresses + if conf.exists(['address']): + wg['address'] = conf.return_values(['address']) + + # get interface addresses (currently effective) - to determine which + # address is no longer valid and needs to be removed + eff_addr = conf.return_effective_values(['address']) + wg['address_remove'] = list_diff(eff_addr, wg['address']) + + # retrieve interface description + if conf.exists(['description']): + wg['description'] = conf.return_value(['description']) + + # disable interface + if conf.exists(['disable']): + wg['disable'] = True + + # local port to listen on + if conf.exists(['port']): + wg['lport'] = conf.return_value(['port']) + + # fwmark value + if conf.exists(['fwmark']): + wg['fwmark'] = int(conf.return_value(['fwmark'])) + + # Maximum Transmission Unit (MTU) + if conf.exists('mtu'): + wg['mtu'] = int(conf.return_value(['mtu'])) + + # retrieve VRF instance + if conf.exists('vrf'): + wg['vrf'] = conf.return_value('vrf') + + # private key + if conf.exists(['private-key']): + wg['pk'] = "{0}/{1}/private.key".format( + kdir, conf.return_value(['private-key'])) + + # peer removal, wg identifies peers by its pubkey + peer_eff = conf.list_effective_nodes(['peer']) + peer_rem = list_diff(peer_eff, conf.list_nodes(['peer'])) + for peer in peer_rem: + wg['peer_remove'].append( + conf.return_effective_value(['peer', peer, 'pubkey'])) + + # peer settings + if conf.exists(['peer']): + for p in conf.list_nodes(['peer']): + # set new config level for this peer + conf.set_level(base + [wg['intf'], 'peer', p]) + peer = { + 'allowed-ips': [], + 'address': '', + 'name': p, + 'persistent_keepalive': '', + 'port': '', + 'psk': '', + 'pubkey': '' + } + + # peer allowed-ips + if conf.exists(['allowed-ips']): + peer['allowed-ips'] = conf.return_values(['allowed-ips']) + + # peer address + if conf.exists(['address']): + peer['address'] = conf.return_value(['address']) + + # peer port + if conf.exists(['port']): + peer['port'] = conf.return_value(['port']) + + # persistent-keepalive + if conf.exists(['persistent-keepalive']): + peer['persistent_keepalive'] = conf.return_value(['persistent-keepalive']) + + # preshared-key + if conf.exists(['preshared-key']): + peer['psk'] = conf.return_value(['preshared-key']) + + # peer pubkeys + if conf.exists(['pubkey']): + key_eff = conf.return_effective_value(['pubkey']) + key_cfg = conf.return_value(['pubkey']) + peer['pubkey'] = key_cfg + + # on a pubkey change we need to remove the pubkey first + # peers are identified by pubkey, so key update means + # peer removal and re-add + if key_eff != key_cfg and key_eff != None: + wg['peer_remove'].append(key_cfg) + + # if a peer is disabled, we have to exec a remove for it's pubkey + if conf.exists(['disable']): + wg['peer_remove'].append(peer['pubkey']) + else: + wg['peer'].append(peer) + return wg -def verify(c): - if not c: - return None +def verify(wg): + interface = wg['intf'] - if c['delete']: - interface = c['intfc'] + if wg['deleted']: is_member, bridge = is_bridge_member(interface) if is_member: # can not use a f'' formatted-string here as bridge would not get @@ -189,98 +196,100 @@ def verify(c): 'is a member of bridge "{1}"!'.format(interface, bridge)) return None - if not os.path.exists(c['pk']): - raise ConfigError( - "No keys found, generate them by executing: \'run generate wireguard [keypair|named-keypairs]\'") - - if not c['delete']: - if not c['addr']: - raise ConfigError("ERROR: IP address required") - if not c['peer']: - raise ConfigError("ERROR: peer required") - for p in c['peer']: - if not c['peer'][p]['allowed-ips']: - raise ConfigError("ERROR: allowed-ips required for peer " + p) - if not c['peer'][p]['pubkey']: - raise ConfigError("peer pubkey required for peer " + p) - - -def apply(c): - # no wg configs left, remove all interface from system - # maybe move it into ifconfig.py - 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)) - # XXX: we are ignoring any errors here - run(f'ip l d dev {wg_intf} >/dev/null') - return None + vrf_name = wg['vrf'] + if vrf_name and vrf_name not in interfaces(): + raise ConfigError(f'VRF "{vrf_name}" does not exist') + + if not os.path.exists(wg['pk']): + raise ConfigError('No keys found, generate them by executing:\n' \ + '"run generate wireguard [keypair|named-keypairs]"') + if not wg['address']: + raise ConfigError(f'IP address required for interface "{interface}"!') + + if not wg['peer']: + raise ConfigError(f'Peer required for interface "{interface}"!') + + # run checks on individual configured WireGuard peer + for peer in wg['peer']: + peer_name = peer['name'] + if not peer['allowed-ips']: + raise ConfigError(f'Peer allowed-ips required for peer "{peer_name}"!') + + if not peer['pubkey']: + raise ConfigError(f'Peer public-key required for peer "{peer_name}"!') + + +def apply(wg): # init wg class - intfc = WireGuardIf(c['intfc']) + w = WireGuardIf(wg['intf']) # single interface removal - if c['delete']: - intfc.remove() + if wg['deleted']: + w.remove() return None - # remove IP addresses - for ip in c['addr_remove']: - intfc.del_addr(ip) + # Configure interface address(es) + # - not longer required addresses get removed first + # - newly addresses will be added second + for addr in wg['address_remove']: + w.del_addr(addr) + for addr in wg['address']: + w.add_addr(addr) - # add IP addresses - for ip in c['addr']: - intfc.add_addr(ip) + # Maximum Transmission Unit (MTU) + w.set_mtu(wg['mtu']) - # interface mtu - intfc.set_mtu(int(c['mtu'])) + # update interface description used e.g. within SNMP + w.set_alias(wg['description']) - # ifalias for snmp from description - intfc.set_alias(str(c['descr'])) + # assign/remove VRF + w.set_vrf(wg['vrf']) # remove peers - if c['peer_remove']: - for pkey in c['peer_remove']: - intfc.remove_peer(pkey) + for pub_key in wg['peer_remove']: + w.remove_peer(pub_key) # peer pubkey # setting up the wg interface - intfc.config['private-key'] = c['pk'] - for p in c['peer']: + w.config['private-key'] = c['pk'] + + for peer in wg['peer']: # peer pubkey - intfc.config['pubkey'] = str(c['peer'][p]['pubkey']) + w.config['pubkey'] = peer['pubkey'] # peer allowed-ips - intfc.config['allowed-ips'] = c['peer'][p]['allowed-ips'] + w.config['allowed-ips'] = peer['allowed-ips'] # local listen port - if c['lport']: - intfc.config['port'] = c['lport'] + if wg['lport']: + w.config['port'] = wg['lport'] # fwmark if c['fwmark']: - intfc.config['fwmark'] = c['fwmark'] + w.config['fwmark'] = wg['fwmark'] + # endpoint - if c['peer'][p]['address'] and c['peer'][p]['port']: - intfc.config['endpoint'] = "{}:{}".format(c['peer'][p]['address'], c['peer'][p]['port']) + if peer['address'] and peer['port']: + w.config['endpoint'] = '{}:{}'.format( + peer['address'], peer['port']) # persistent-keepalive - if 'persistent-keepalive' in c['peer'][p]: - intfc.config['keepalive'] = c['peer'][p]['persistent-keepalive'] + if peer['persistent_keepalive']: + w.config['keepalive'] = peer['persistent_keepalive'] # maybe move it into ifconfig.py # preshared-key - needs to be read from a file - if 'psk' in c['peer'][p]: + if peer['psk']: psk_file = '/config/auth/wireguard/psk' - old_umask = os.umask(0o077) - open(psk_file, 'w').write(str(c['peer'][p]['psk'])) - os.umask(old_umask) - intfc.config['psk'] = psk_file - intfc.update() + with open(psk_file, 'w') as f: + f.write(peer['psk']) + w.config['psk'] = psk_file + + w.update() - # interface state - intfc.set_admin_state(c['state']) + # Enable/Disable interface + if wg['disable']: + w.set_admin_state('down') + else: + w.set_admin_state('up') return None @@ -293,4 +302,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - sys.exit(1) + exit(1) diff --git a/src/conf_mode/interfaces-wireless.py b/src/conf_mode/interfaces-wireless.py index 709085b0f..138f27755 100755 --- a/src/conf_mode/interfaces-wireless.py +++ b/src/conf_mode/interfaces-wireless.py @@ -29,7 +29,7 @@ from vyos.configdict import list_diff, vlan_to_dict from vyos.defaults import directories as vyos_data_dir from vyos.ifconfig import WiFiIf from vyos.ifconfig_vlan import apply_vlan_config, verify_vlan_config -from vyos.util import process_running, chmod_x, chown_file, run, is_bridge_member +from vyos.util import process_running, chmod_x, chown, run, is_bridge_member from vyos import ConfigError user = 'root' @@ -121,7 +121,7 @@ def get_conf_file(conf_type, intf): if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) chmod_x(cfg_dir) - chown_file(cfg_dir, user, group) + chown(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file @@ -133,7 +133,7 @@ def get_pid(conf_type, intf): if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) chmod_x(cfg_dir) - chown_file(cfg_dir, user, group) + chown(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.pid'.format(intf) return cfg_file @@ -146,7 +146,7 @@ def get_wpa_suppl_config_name(intf): if not os.path.exists(cfg_dir): os.mkdir(cfg_dir) chmod_x(cfg_dir) - chown_file(cfg_dir, user, group) + chown(cfg_dir, user, group) cfg_file = cfg_dir + r'/{}.cfg'.format(intf) return cfg_file diff --git a/src/conf_mode/interfaces-wirelessmodem.py b/src/conf_mode/interfaces-wirelessmodem.py index 49445aaa4..e5af37b8f 100755 --- a/src/conf_mode/interfaces-wirelessmodem.py +++ b/src/conf_mode/interfaces-wirelessmodem.py @@ -23,7 +23,7 @@ from netifaces import interfaces from vyos.config import Config from vyos.defaults import directories as vyos_data_dir -from vyos.util import chown_file, chmod_x, cmd, run, is_bridge_member +from vyos.util import chown, chmod_x, cmd, run, is_bridge_member from vyos import ConfigError default_config_data = { @@ -219,7 +219,7 @@ def apply(wwan): intf = wwan['intf'] cmd(f'systemctl start ppp@{intf}.service') # make logfile owned by root / vyattacfg - chown_file(wwan['logfile'], 'root', 'vyattacfg') + chown(wwan['logfile'], 'root', 'vyattacfg') return None diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 07466f3aa..586424c09 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -195,6 +195,7 @@ def apply(vrf_config): # # - https://github.com/torvalds/linux/blob/master/Documentation/networking/vrf.txt # - https://github.com/Mellanox/mlxsw/wiki/Virtual-Routing-and-Forwarding-(VRF) + # - https://github.com/Mellanox/mlxsw/wiki/L3-Tunneling # - https://netdevconf.info/1.1/proceedings/slides/ahern-vrf-tutorial.pdf # - https://netdevconf.info/1.2/slides/oct6/02_ahern_what_is_l3mdev_slides.pdf diff --git a/src/helpers/vyos-merge-config.py b/src/helpers/vyos-merge-config.py index 6546c03e3..10a5ea4bc 100755 --- a/src/helpers/vyos-merge-config.py +++ b/src/helpers/vyos-merge-config.py @@ -17,13 +17,13 @@ import sys import os +import subprocess import tempfile import vyos.defaults import vyos.remote from vyos.config import Config from vyos.configtree import ConfigTree from vyos.migrator import Migrator, VirtualMigrator -from vyos.util import cmd if (len(sys.argv) < 2): @@ -100,10 +100,12 @@ if path: add_cmds = [ cmd for cmd in add_cmds if path in cmd ] for cmd in add_cmds: + cmd = "/opt/vyatta/sbin/my_" + cmd + try: - cmd(f'/opt/vyatta/sbin/my_{cmd}', message='Called process error') - except OSError as err: - print(err) + subprocess.check_call(cmd, shell=True) + except subprocess.CalledProcessError as err: + print("Called process error: {}.".format(err)) if effective_config.session_changed(): print("Merge complete. Use 'commit' to make changes effective.") diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py index 93c2444b9..dad78d7e8 100755 --- a/src/op_mode/dns_forwarding_reset.py +++ b/src/op_mode/dns_forwarding_reset.py @@ -21,13 +21,12 @@ import os -import sys import argparse -import vyos.config +from sys import exit +from vyos.config import Config from vyos.util import run - parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Reset all cache") parser.add_argument("domain", type=str, nargs="?", help="Domain to reset cache entries for") @@ -36,16 +35,18 @@ if __name__ == '__main__': args = parser.parse_args() # Do nothing if service is not configured - c = vyos.config.Config() - if not c.exists_effective('service dns forwarding'): + c = Config() + if not c.exists_effective(['service', 'dns', 'forwarding']): print("DNS forwarding is not configured") - sys.exit(0) + exit(0) if args.all: run("rec_control wipe-cache \'.$\'") - sys.exit(1) + exit(0) + elif args.domain: run("rec_control wipe-cache \'{0}$\'".format(args.domain)) + else: parser.print_help() - sys.exit(1) + exit(1) diff --git a/src/op_mode/lldp_op.py b/src/op_mode/lldp_op.py index c8a5543b6..5d48e3210 100755 --- a/src/op_mode/lldp_op.py +++ b/src/op_mode/lldp_op.py @@ -23,6 +23,7 @@ from sys import exit from tabulate import tabulate from vyos.util import popen +from vyos.config import Config parser = argparse.ArgumentParser() parser.add_argument("-a", "--all", action="store_true", help="Show LLDP neighbors on all interfaces") @@ -141,6 +142,11 @@ if __name__ == '__main__': args = parser.parse_args() tmp = { 'neighbors' : [] } + c = Config() + if not c.exists_effective(['service', 'lldp']): + print('Service LLDP is not configured') + exit(0) + if args.all: neighbors = minidom.parseString(_get_neighbors()) for neighbor in neighbors.getElementsByTagName('interface'): diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py index d940d79eb..1b90f4fa7 100755 --- a/src/op_mode/wireguard.py +++ b/src/op_mode/wireguard.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2018-2020 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import argparse import os @@ -27,7 +25,7 @@ from vyos.ifconfig import WireGuardIf from vyos import ConfigError from vyos.config import Config -from vyos.util import run +from vyos.util import cmd, run dir = r'/config/auth/wireguard' psk = dir + '/preshared.key' @@ -88,10 +86,11 @@ def genpsk(): it's stored only in the cli config """ - run('wg genpsk') + psk = cmd('wg genpsk') + print(psk) def list_key_dirs(): - """ lists all dirs under /config/auth/wireguard """ + """ lists all dirs under /config/auth/wireguard """ if os.path.exists(dir): nks = next(os.walk(dir))[1] for nk in nks: |