summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--data/templates/accel-ppp/sstp.config.tmpl29
-rw-r--r--debian/control4
-rw-r--r--interface-definitions/vpn_sstp.xml.in15
-rw-r--r--python/vyos/debug.py9
-rwxr-xr-xscripts/build-command-templates2
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py56
-rwxr-xr-xsrc/helpers/validate-value.py45
-rwxr-xr-xsrc/services/vyos-http-api-server92
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}")