diff options
| -rw-r--r-- | data/templates/accel-ppp/sstp.config.tmpl | 29 | ||||
| -rw-r--r-- | debian/control | 4 | ||||
| -rw-r--r-- | interface-definitions/vpn_sstp.xml.in | 15 | ||||
| -rw-r--r-- | python/vyos/debug.py | 9 | ||||
| -rwxr-xr-x | scripts/build-command-templates | 2 | ||||
| -rwxr-xr-x | src/conf_mode/vpn_sstp.py | 56 | ||||
| -rwxr-xr-x | src/helpers/validate-value.py | 45 | ||||
| -rwxr-xr-x | src/services/vyos-http-api-server | 92 | 
8 files changed, 138 insertions, 114 deletions
diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index c3dc83429..411fca489 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -9,6 +9,9 @@ chap-secrets  radius  {% endif -%}  ippool +ipv6pool +ipv6_nd +ipv6_dhcp  {% for proto in auth_proto %}  {{proto}} @@ -51,6 +54,14 @@ dns{{ loop.index }}={{ dns }}  {% endfor -%}  {% endif %} +{% if dnsv6 %} +[ipv6-dns] +{% for dns in dnsv6 -%} +{{ dns }} +{% endfor -%} +{% endif %} + +  {% if auth_mode == 'local' %}  [chap-secrets]  chap-secrets={{ chap_secrets_file }} @@ -87,6 +98,9 @@ check-ip=1  {% if mtu %}  mtu={{ mtu }}  {% endif -%} +{% if client_ipv6_pool %} +ipv6=allow +{% endif %}  {% if ppp_mppe %}  mppe={{ ppp_mppe }} @@ -101,6 +115,21 @@ lcp-echo-failure={{ ppp_echo_failure }}  lcp-echo-timeout={{ ppp_echo_timeout }}  {% endif %} +{% if client_ipv6_pool %} +[ipv6-pool] +{% for p in client_ipv6_pool %} +{{ p.prefix }},{{ p.mask }} +{% endfor %} +{% for p in client_ipv6_delegate_prefix %} +delegate={{ p.prefix }},{{ p.mask }} +{% endfor %} +{% endif %} + +{% if client_ipv6_delegate_prefix %} +[ipv6-dhcp] +verbose=1 +{% endif %} +  {% if radius_shaper_attr %}  [shaper]  verbose=1 diff --git a/debian/control b/debian/control index 5c176f40a..ab0fc0b29 100644 --- a/debian/control +++ b/debian/control @@ -28,7 +28,8 @@ Depends: python3,    python3-isc-dhcp-leases,    python3-hurry.filesize,    python3-vici (>= 5.7.2), -  python3-bottle, +  python3-flask, +  python3-waitress,    python3-netaddr,    python3-zmq,    cron, @@ -90,6 +91,7 @@ Depends: python3,    python3-certbot-nginx,    pppoe,    salt-minion, +  vyos-utils,    ${shlibs:Depends},    ${misc:Depends}  Description: VyOS configuration scripts and data diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index 7e4471015..f0c93b882 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -207,19 +207,8 @@                    </leafNode>                  </children>                </node> -              <leafNode name="name-server"> -                <properties> -                  <help>DNS servers propagated to clients</help> -                      <valueHelp> -                        <format>ipv4</format> -                        <description>IPv4 address</description> -                      </valueHelp> -                      <constraint> -                        <validator name="ipv4-address"/> -                      </constraint> -                      <multi/> -                </properties> -              </leafNode> +              #include <include/accel-client-ipv6-pool.xml.in> +              #include <include/accel-name-server.xml.in>                #include <include/interface-mtu-68-1500.xml.i>              </children>            </node> diff --git a/python/vyos/debug.py b/python/vyos/debug.py index 91431a7bb..6ce42b173 100644 --- a/python/vyos/debug.py +++ b/python/vyos/debug.py @@ -15,7 +15,7 @@  import os  import sys - +from datetime import datetime  def message(message, flag='', destination=sys.stdout):      """ @@ -46,7 +46,7 @@ def message(message, flag='', destination=sys.stdout):          mask = os.umask(0o111)          with open(logfile, 'a') as f: -            f.write(_format('log', message)) +            f.write(_timed(_format('log', message)))      finally:          os.umask(mask) @@ -81,6 +81,11 @@ def enabled(flag):      return _fromenv(flag) or _fromfile(flag) +def _timed(message): +    now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') +    return f'{now} {message}' + +  def _remove_invisible(string):      for char in ('\0', '\a', '\b', '\f', '\v'):          string = string.replace(char, '') diff --git a/scripts/build-command-templates b/scripts/build-command-templates index c6534a6d8..767517b29 100755 --- a/scripts/build-command-templates +++ b/scripts/build-command-templates @@ -149,7 +149,7 @@ def get_properties(p):          regex_args = " ".join(map(lambda s: "--regex \\\'{0}\\\'".format(s), regexes))          validator_args = " ".join(map(lambda s: "--exec \\\"{0}\\\"".format(s), validators)) -        validator_script = '${vyos_libexec_dir}/validate-value.py' +        validator_script = '${vyos_libexec_dir}/validate-value'          validator_string = "exec \"{0} {1} {2} --value \\\'$VAR(@)\\\'\"; \"{3}\"".format(validator_script, regex_args, validator_args, error_msg)          props["constraint"] = validator_string diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index d250cd3b0..7c3e3f515 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -22,10 +22,10 @@ from copy import deepcopy  from stat import S_IRUSR, S_IWUSR, S_IRGRP  from vyos.config import Config -from vyos import ConfigError -from vyos.util import call, run, get_half_cpus  from vyos.template import render - +from vyos.util import call, run, get_half_cpus +from vyos.validate import is_ipv4 +from vyos import ConfigError  sstp_conf = '/run/accel-pppd/sstp.conf'  sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' @@ -35,7 +35,12 @@ default_config_data = {      'auth_mode' : 'local',      'auth_proto' : ['auth_mschap_v2'],      'chap_secrets_file': sstp_chap_secrets, # used in Jinja2 template +    'client_ip_pool' : [], +    'client_ipv6_pool': [], +    'client_ipv6_delegate_prefix': [],      'client_gateway': '', +    'dnsv4' : [], +    'dnsv6' : [],      'radius_server' : [],      'radius_acct_tmo' : '3',      'radius_max_try' : '3', @@ -49,8 +54,6 @@ default_config_data = {      'ssl_ca' : '',      'ssl_cert' : '',      'ssl_key' : '', -    'client_ip_pool' : [], -    'dnsv4' : [],      'mtu' : '',      'ppp_mppe' : 'prefer',      'ppp_echo_failure' : '', @@ -210,7 +213,7 @@ def get_config():      # -    # read in client ip pool settings +    # read in client IPv4 pool      conf.set_level(base_path + ['network-settings', 'client-ip-settings'])      if conf.exists(['subnet']):          sstp['client_ip_pool'] = conf.return_values(['subnet']) @@ -219,10 +222,41 @@ def get_config():          sstp['client_gateway'] = conf.return_value(['gateway-address'])      # +    # read in client IPv6 pool +    conf.set_level(base_path + ['network-settings', 'client-ipv6-pool']) +    if conf.exists(['prefix']): +        for prefix in conf.list_nodes(['prefix']): +            tmp = { +                'prefix': prefix, +                'mask': '64' +            } + +            if conf.exists(['prefix', prefix, 'mask']): +                tmp['mask'] = conf.return_value(['prefix', prefix, 'mask']) + +            sstp['client_ipv6_pool'].append(tmp) + +    if conf.exists(['delegate']): +        for prefix in conf.list_nodes(['delegate']): +            tmp = { +                'prefix': prefix, +                'mask': '' +            } + +            if conf.exists(['delegate', prefix, 'delegation-prefix']): +                tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix']) + +            sstp['client_ipv6_delegate_prefix'].append(tmp) + +    #      # read in network settings      conf.set_level(base_path + ['network-settings'])      if conf.exists(['name-server']): -        sstp['dnsv4'] = conf.return_values(['name-server']) +        for name_server in conf.return_values(['name-server']): +            if is_ipv4(name_server): +                sstp['dnsv4'].append(name_server) +            else: +                sstp['dnsv6'].append(name_server)      if conf.exists(['mtu']):          sstp['mtu'] = conf.return_value(['mtu']) @@ -275,6 +309,14 @@ def verify(sstp):      if len(sstp['dnsv4']) > 2:          raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') +    # check ipv6 +    if sstp['client_ipv6_delegate_prefix'] and not sstp['client_ipv6_pool']: +        raise ConfigError('IPv6 prefix delegation requires client-ipv6-pool prefix') + +    for prefix in sstp['client_ipv6_delegate_prefix']: +        if not prefix['mask']: +            raise ConfigError('Delegation-prefix required for individual delegated networks') +      if not sstp['ssl_ca'] or not sstp['ssl_cert'] or not sstp['ssl_key']:          raise ConfigError('One or more SSL certificates missing') diff --git a/src/helpers/validate-value.py b/src/helpers/validate-value.py deleted file mode 100755 index a58ba61d1..000000000 --- a/src/helpers/validate-value.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 - -import re -import os -import sys -import argparse - -from vyos.util import call - -parser = argparse.ArgumentParser() -parser.add_argument('--regex', action='append') -parser.add_argument('--exec', action='append') -parser.add_argument('--value', action='store') - -args = parser.parse_args() - -debug = False - -# Multiple arguments work like logical OR - -try: -    for r in args.regex: -        if re.fullmatch(r, args.value): -            sys.exit(0) -except Exception as exn: -    if debug: -        print(exn) -    else: -        pass - -try: -    for cmd in args.exec: -        cmd = "{0} {1}".format(cmd, args.value) -        if debug: -            print(cmd) -        res = call(cmd) -        if res == 0: -            sys.exit(0) -except Exception as exn: -    if debug: -        print(exn) -    else: -        pass - -sys.exit(1) diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index c36cbd640..4c41fa96d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -26,7 +26,8 @@ import signal  import vyos.config -import bottle +from flask import Flask, request +from waitress import serve  from functools import wraps @@ -37,7 +38,7 @@ from vyos.config import VyOSError  DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf'  CFG_GROUP = 'vyattacfg' -app = bottle.default_app() +app = Flask(__name__)  # Giant lock!  lock = threading.Lock() @@ -55,18 +56,31 @@ def check_auth(key_list, key):      return id  def error(code, msg): -    bottle.response.status = code      resp = {"success": False, "error": msg, "data": None} -    return json.dumps(resp) +    return json.dumps(resp), code  def success(data):      resp = {"success": True, "data": data, "error": None}      return json.dumps(resp) +def get_command(f): +    @wraps(f) +    def decorated_function(*args, **kwargs): +        cmd = request.form.get("data") +        if not cmd: +            return error(400, "Non-empty data field is required") +        try: +            cmd = json.loads(cmd) +        except Exception as e: +            return error(400, "Failed to parse JSON: {0}".format(e)) +        return f(cmd, *args, **kwargs) + +    return decorated_function +  def auth_required(f):      @wraps(f)      def decorated_function(*args, **kwargs): -        key = bottle.request.forms.get("key") +        key = request.form.get("key")          api_keys = app.config['vyos_keys']          id = check_auth(api_keys, key)          if not id: @@ -75,28 +89,20 @@ def auth_required(f):      return decorated_function -@app.route('/configure', method='POST') +@app.route('/configure', methods=['POST']) +@get_command  @auth_required -def configure(): +def configure_op(commands):      session = app.config['vyos_session']      env = session.get_session_env()      config = vyos.config.Config(session_env=env) -    strict_field = bottle.request.forms.get("strict") +    strict_field = request.form.get("strict")      if strict_field == "true":          strict = True      else:          strict = False -    commands = bottle.request.forms.get("data") -    if not commands: -        return error(400, "Non-empty data field is required") -    else: -        try: -            commands = json.loads(commands) -        except Exception as e: -            return error(400, "Failed to parse JSON: {0}".format(e)) -      # Allow users to pass just one command      if not isinstance(commands, list):          commands = [commands] @@ -186,16 +192,14 @@ def configure():      else:          return success(None) -@app.route('/retrieve', method='POST') +@app.route('/retrieve', methods=['POST']) +@get_command  @auth_required -def get_value(): +def retrieve_op(command):      session = app.config['vyos_session']      env = session.get_session_env()      config = vyos.config.Config(session_env=env) -    command = bottle.request.forms.get("data") -    command = json.loads(command) -      try:          op = command['op']          path = " ".join(command['path']) @@ -229,20 +233,20 @@ def get_value():              return error(400, "\"{0}\" is not a valid operation".format(op))      except VyOSError as e:          return error(400, str(e)) +    except ConfigSessionError as e: +        return error(400, str(e))      except Exception as e:          print(traceback.format_exc(), file=sys.stderr)          return error(500, "An internal error occured. Check the logs for details.")      return success(res) -@app.route('/config-file', method='POST') +@app.route('/config-file', methods=['POST']) +@get_command  @auth_required -def config_file_op(): +def config_file_op(command):      session = app.config['vyos_session'] -    command = bottle.request.forms.get("data") -    command = json.loads(command) -      try:          op = command['op']      except KeyError: @@ -264,7 +268,7 @@ def config_file_op():              res = session.commit()          else:              return error(400, "\"{0}\" is not a valid operation".format(op)) -    except VyOSError as e: +    except ConfigSessionError as e:          return error(400, str(e))      except Exception as e:          print(traceback.format_exc(), file=sys.stderr) @@ -272,14 +276,12 @@ def config_file_op():      return success(res) -@app.route('/image', method='POST') +@app.route('/image', methods=['POST']) +@get_command  @auth_required -def config_file_op(): +def image_op(command):      session = app.config['vyos_session'] -    command = bottle.request.forms.get("data") -    command = json.loads(command) -      try:          op = command['op']      except KeyError: @@ -300,7 +302,7 @@ def config_file_op():              res = session.remove_image(name)          else:              return error(400, "\"{0}\" is not a valid operation".format(op)) -    except VyOSError as e: +    except ConfigSessionError as e:          return error(400, str(e))      except Exception as e:          print(traceback.format_exc(), file=sys.stderr) @@ -309,14 +311,12 @@ def config_file_op():      return success(res) -@app.route('/generate', method='POST') +@app.route('/generate', methods=['POST']) +@get_command  @auth_required -def generate_op(): +def generate_op(command):      session = app.config['vyos_session'] -    command = bottle.request.forms.get("data") -    command = json.loads(command) -      try:          op = command['op']          path = command['path'] @@ -339,14 +339,12 @@ def generate_op():      return success(res) -@app.route('/show', method='POST') +@app.route('/show', methods=['POST']) +@get_command  @auth_required -def show_op(): +def show_op(command):      session = app.config['vyos_session'] -    command = bottle.request.forms.get("data") -    command = json.loads(command) -      try:          op = command['op']          path = command['path'] @@ -398,4 +396,8 @@ if __name__ == '__main__':      signal.signal(signal.SIGTERM, sig_handler) -    bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True) +    try: +        serve(app, host=server_config["listen_address"], +                   port=server_config["port"]) +    except OSError as e: +        print(f"OSError {e}")  | 
