From d55050dcb8806a982b0394dcde00c1814499d9f3 Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 6 May 2020 21:41:15 +0200 Subject: sstp: T2392: add initial IPv6 support New commands added: * set vpn sstp network-settings client-ipv6-pool prefix 2001:db8::/64 mask 112 * set vpn sstp network-settings client-ipv6-pool delegate 2001:db8:100::/48 delegation-prefix 64 --- src/conf_mode/vpn_sstp.py | 43 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index d250cd3b0..6d9496012 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -35,7 +35,11 @@ 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' : [], 'radius_server' : [], 'radius_acct_tmo' : '3', 'radius_max_try' : '3', @@ -49,8 +53,6 @@ default_config_data = { 'ssl_ca' : '', 'ssl_cert' : '', 'ssl_key' : '', - 'client_ip_pool' : [], - 'dnsv4' : [], 'mtu' : '', 'ppp_mppe' : 'prefer', 'ppp_echo_failure' : '', @@ -210,7 +212,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']) @@ -218,6 +220,33 @@ def get_config(): if conf.exists(['gateway-address']): sstp['client_gateway'] = conf.return_value(['gateway-address']) + # + # read in client IPv6 pool + conf.set_level(base_path + ['network-settings', 'client-ipv6-pool']) + if conf.exists(['prefix']): + for prefix in conf.list_nodes(['prefix']): + tmp = { + 'prefix': prefix, + 'mask': '64' + } + + if conf.exists(['prefix', prefix, 'mask']): + tmp['mask'] = conf.return_value(['prefix', prefix, 'mask']) + + sstp['client_ipv6_pool'].append(tmp) + + if conf.exists(['delegate']): + for prefix in conf.list_nodes(['delegate']): + tmp = { + 'prefix': prefix, + 'mask': '' + } + + if conf.exists(['delegate', prefix, 'delegation-prefix']): + tmp['mask'] = conf.return_value(['delegate', prefix, 'delegation-prefix']) + + sstp['client_ipv6_delegate_prefix'].append(tmp) + # # read in network settings conf.set_level(base_path + ['network-settings']) @@ -275,6 +304,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') -- cgit v1.2.3 From ed22334321d3b6f27b5d695a4f984257b909f78b Mon Sep 17 00:00:00 2001 From: Christian Poessinger Date: Wed, 6 May 2020 21:44:07 +0200 Subject: sstp: T2392: add IPv6 DNS support New command added: * set vpn sstp network-settings name-server 2001:db8::1111 --- data/templates/accel-ppp/sstp.config.tmpl | 8 ++++++++ interface-definitions/vpn_sstp.xml.in | 14 +------------- src/conf_mode/vpn_sstp.py | 13 +++++++++---- 3 files changed, 18 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index e0a48a44e..411fca489 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -54,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 }} diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index 4ce231e0f..f0c93b882 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -207,20 +207,8 @@ - - - DNS servers propagated to clients - - ipv4 - IPv4 address - - - - - - - #include + #include #include diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 6d9496012..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' @@ -40,6 +40,7 @@ default_config_data = { 'client_ipv6_delegate_prefix': [], 'client_gateway': '', 'dnsv4' : [], + 'dnsv6' : [], 'radius_server' : [], 'radius_acct_tmo' : '3', 'radius_max_try' : '3', @@ -251,7 +252,11 @@ def get_config(): # 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']) -- cgit v1.2.3 From 3a24bd72f6eb10f5d38d053f0b234793865f6821 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 6 May 2020 12:13:52 -0500 Subject: http api: function names should be consistent --- src/services/vyos-http-api-server | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index c36cbd640..14cdd8437 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -77,7 +77,7 @@ def auth_required(f): @app.route('/configure', method='POST') @auth_required -def configure(): +def configure_op(): session = app.config['vyos_session'] env = session.get_session_env() config = vyos.config.Config(session_env=env) @@ -188,7 +188,7 @@ def configure(): @app.route('/retrieve', method='POST') @auth_required -def get_value(): +def retrieve_op(): session = app.config['vyos_session'] env = session.get_session_env() config = vyos.config.Config(session_env=env) @@ -274,7 +274,7 @@ def config_file_op(): @app.route('/image', method='POST') @auth_required -def config_file_op(): +def image_op(): session = app.config['vyos_session'] command = bottle.request.forms.get("data") -- cgit v1.2.3 From 25c65547458a3a2f4f4f8b1b70541229f3cbcc0c Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 6 May 2020 12:35:16 -0500 Subject: http api: catch appropriate errors --- src/services/vyos-http-api-server | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 14cdd8437..4928b0bae 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -229,6 +229,8 @@ def retrieve_op(): 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.") @@ -264,7 +266,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) @@ -300,7 +302,7 @@ def image_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) -- cgit v1.2.3 From dbe9f73cc9180b5c0d06007476d7120cde51725d Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 6 May 2020 16:32:02 -0500 Subject: http api: use decorator to get command data from request --- src/services/vyos-http-api-server | 56 ++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4928b0bae..5cad67eb7 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -63,6 +63,20 @@ 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 = bottle.request.forms.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): @@ -76,8 +90,9 @@ def auth_required(f): return decorated_function @app.route('/configure', method='POST') +@get_command @auth_required -def configure_op(): +def configure_op(commands): session = app.config['vyos_session'] env = session.get_session_env() config = vyos.config.Config(session_env=env) @@ -88,15 +103,6 @@ def configure_op(): 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] @@ -187,15 +193,13 @@ def configure_op(): return success(None) @app.route('/retrieve', method='POST') +@get_command @auth_required -def retrieve_op(): +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']) @@ -238,13 +242,11 @@ def retrieve_op(): return success(res) @app.route('/config-file', method='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: @@ -275,13 +277,11 @@ def config_file_op(): return success(res) @app.route('/image', method='POST') +@get_command @auth_required -def image_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: @@ -312,13 +312,11 @@ def image_op(): @app.route('/generate', method='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'] @@ -342,13 +340,11 @@ def generate_op(): return success(res) @app.route('/show', method='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'] -- cgit v1.2.3 From c4312498ebe0643061f1d06ad9655d49fb9d9af7 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 6 May 2020 17:43:33 -0500 Subject: http api: T2395: replace bottle with flask as microframework --- debian/control | 2 +- src/services/vyos-http-api-server | 27 +++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/debian/control b/debian/control index 5c176f40a..592d5153d 100644 --- a/debian/control +++ b/debian/control @@ -28,7 +28,7 @@ Depends: python3, python3-isc-dhcp-leases, python3-hurry.filesize, python3-vici (>= 5.7.2), - python3-bottle, + python3-flask, python3-netaddr, python3-zmq, cron, diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 5cad67eb7..4a653bb66 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -26,7 +26,7 @@ import signal import vyos.config -import bottle +from flask import Flask, request from functools import wraps @@ -37,7 +37,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,9 +55,8 @@ 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} @@ -66,7 +65,7 @@ def success(data): def get_command(f): @wraps(f) def decorated_function(*args, **kwargs): - cmd = bottle.request.forms.get("data") + cmd = request.form.get("data") if not cmd: return error(400, "Non-empty data field is required") try: @@ -80,7 +79,7 @@ def get_command(f): 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: @@ -89,7 +88,7 @@ def auth_required(f): return decorated_function -@app.route('/configure', method='POST') +@app.route('/configure', methods=['POST']) @get_command @auth_required def configure_op(commands): @@ -97,7 +96,7 @@ def configure_op(commands): 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: @@ -192,7 +191,7 @@ def configure_op(commands): else: return success(None) -@app.route('/retrieve', method='POST') +@app.route('/retrieve', methods=['POST']) @get_command @auth_required def retrieve_op(command): @@ -241,7 +240,7 @@ def retrieve_op(command): return success(res) -@app.route('/config-file', method='POST') +@app.route('/config-file', methods=['POST']) @get_command @auth_required def config_file_op(command): @@ -276,7 +275,7 @@ def config_file_op(command): return success(res) -@app.route('/image', method='POST') +@app.route('/image', methods=['POST']) @get_command @auth_required def image_op(command): @@ -311,7 +310,7 @@ def image_op(command): return success(res) -@app.route('/generate', method='POST') +@app.route('/generate', methods=['POST']) @get_command @auth_required def generate_op(command): @@ -339,7 +338,7 @@ def generate_op(command): return success(res) -@app.route('/show', method='POST') +@app.route('/show', methods=['POST']) @get_command @auth_required def show_op(command): @@ -396,4 +395,4 @@ if __name__ == '__main__': signal.signal(signal.SIGTERM, sig_handler) - bottle.run(app, host=server_config["listen_address"], port=server_config["port"], debug=True) + app.run(host=server_config["listen_address"], port=server_config["port"], debug=True) -- cgit v1.2.3 From 7b31dd720d96bc481323e3b00100da718f551cd5 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Wed, 6 May 2020 17:49:12 -0500 Subject: http api: T2395: add waitress as production WSGI server --- debian/control | 1 + src/services/vyos-http-api-server | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/debian/control b/debian/control index 592d5153d..32c0286a4 100644 --- a/debian/control +++ b/debian/control @@ -29,6 +29,7 @@ Depends: python3, python3-hurry.filesize, python3-vici (>= 5.7.2), python3-flask, + python3-waitress, python3-netaddr, python3-zmq, cron, diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4a653bb66..4c41fa96d 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -27,6 +27,7 @@ import signal import vyos.config from flask import Flask, request +from waitress import serve from functools import wraps @@ -395,4 +396,8 @@ if __name__ == '__main__': signal.signal(signal.SIGTERM, sig_handler) - app.run(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}") -- cgit v1.2.3 From 29dee3abb55d0f0c6b91b311f30521b45d7e46b6 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Thu, 7 May 2020 23:00:46 +0300 Subject: T2431: use native versions of validate-value and numeric validator. --- debian/control | 1 + scripts/build-command-templates | 2 +- src/helpers/validate-value.py | 45 ------------------------ src/validators/numeric | 78 ----------------------------------------- 4 files changed, 2 insertions(+), 124 deletions(-) delete mode 100755 src/helpers/validate-value.py delete mode 100755 src/validators/numeric (limited to 'src') diff --git a/debian/control b/debian/control index 32c0286a4..ab0fc0b29 100644 --- a/debian/control +++ b/debian/control @@ -91,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/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/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/validators/numeric b/src/validators/numeric deleted file mode 100755 index 2cd5178b9..000000000 --- a/src/validators/numeric +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 -# -# numeric value validator -# -# Copyright (C) 2017 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# 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 argparse - -parser = argparse.ArgumentParser() -parser.add_argument("-f", "--float", action="store_true", help="Accept floating point values") -group = parser.add_mutually_exclusive_group() -group.add_argument("-r", "--range", type=str, help="Check if the number is within range (inclusive), example: 1024-65535", action='append') -group.add_argument("-n", "--non-negative", action="store_true", help="Check if the number is non-negative (>= 0)") -group.add_argument("-p", "--positive", action="store_true", help="Check if the number is positive (> 0)") -parser.add_argument("number", type=str, help="Number to validate") - -args = parser.parse_args() - -# Try to load the argument -number = None -if args.float: - try: - number = float(args.number) - except: - print("{0} is not a valid floating point number".format(args.number), file=sys.stderr) - sys.exit(1) -else: - try: - number = int(args.number) - except: - print("{0} is not a valid integer number".format(args.number), file=sys.stderr) - sys.exit(1) - -if args.range: - valid = False - for r in args.range: - try: - list = r.split('-') - lower = int(list[0]) - upper = int(list[1]) - except: - print("{0} is not a valid number range",format(args.range), file=sys.stderr) - sys.exit(1) - - if (number >= lower) and (number <= upper): - valid = True - # end for - - if not valid: - if len(args.range) > 1: - err_msg = "Number {0} is not in any of the ranges {1}".format(number, args.range) - else: - err_msg = "Number {0} is not in the range {1}".format(number, args.range[0]) - print(err_msg, file=sys.stderr) - sys.exit(1) -elif args.non_negative: - if number < 0: - print("Number should be non-negative", file=sys.stderr) - sys.exit(1) -elif args.positive: - if number <= 0: - print("Number should be positive", file=sys.stderr) - sys.exit(1) - -- cgit v1.2.3