diff options
-rw-r--r-- | data/templates/frr/policy.frr.tmpl | 4 | ||||
-rw-r--r-- | debian/control | 1 | ||||
-rw-r--r-- | interface-definitions/lldp.xml.in | 3 | ||||
-rw-r--r-- | interface-definitions/policy.xml.in | 4 | ||||
-rw-r--r-- | op-mode-definitions/wireguard.xml.in | 54 | ||||
-rw-r--r-- | python/vyos/frr.py | 5 | ||||
-rw-r--r-- | python/vyos/ifconfig/tunnel.py | 1 | ||||
-rw-r--r-- | python/vyos/remote.py | 64 | ||||
-rwxr-xr-x | smoketest/scripts/cli/test_protocols_bgp.py | 24 | ||||
-rwxr-xr-x | src/conf_mode/policy.py | 73 | ||||
-rwxr-xr-x | src/op_mode/show_dhcpv6.py | 2 | ||||
-rwxr-xr-x | src/op_mode/wireguard_client.py | 118 |
12 files changed, 308 insertions, 45 deletions
diff --git a/data/templates/frr/policy.frr.tmpl b/data/templates/frr/policy.frr.tmpl index 4f4b8705d..881afa21f 100644 --- a/data/templates/frr/policy.frr.tmpl +++ b/data/templates/frr/policy.frr.tmpl @@ -118,7 +118,9 @@ ip prefix-list {{ prefix_list }} description {{ prefix_list_config.description } {% endif %} {% if prefix_list_config.rule is defined and prefix_list_config.rule is not none %} {% for rule, rule_config in prefix_list_config.rule.items() | natural_sort %} +{% if rule_config.prefix is defined and rule_config.prefix is not none %} ip prefix-list {{ prefix_list }} seq {{ rule }} {{ rule_config.action }} {{ rule_config.prefix }} {{ 'ge ' + rule_config.ge if rule_config.ge is defined }} {{ 'le ' + rule_config.le if rule_config.le is defined }} +{% endif %} {% endfor %} {% endif %} {% endfor %} @@ -131,7 +133,9 @@ ipv6 prefix-list {{ prefix_list }} description {{ prefix_list_config.description {% endif %} {% if prefix_list_config.rule is defined and prefix_list_config.rule is not none %} {% for rule, rule_config in prefix_list_config.rule.items() | natural_sort %} +{% if rule_config.prefix is defined and rule_config.prefix is not none %} ipv6 prefix-list {{ prefix_list }} seq {{ rule }} {{ rule_config.action }} {{ rule_config.prefix }} {{ 'ge ' + rule_config.ge if rule_config.ge is defined }} {{ 'le ' + rule_config.le if rule_config.le is defined }} +{% endif %} {% endfor %} {% endif %} {% endfor %} diff --git a/debian/control b/debian/control index c42915cb7..851152d95 100644 --- a/debian/control +++ b/debian/control @@ -113,6 +113,7 @@ Depends: python3-waitress, python3-xmltodict, python3-zmq, + qrencode, radvd, salt-minion, snmp, diff --git a/interface-definitions/lldp.xml.in b/interface-definitions/lldp.xml.in index 9fdffcea1..e14abae14 100644 --- a/interface-definitions/lldp.xml.in +++ b/interface-definitions/lldp.xml.in @@ -152,6 +152,9 @@ <leafNode name="management-address"> <properties> <help>Management IP Address</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> <valueHelp> <format>ipv4</format> <description>IPv4 Management Address</description> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index cd22052f0..08e2ce2c6 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -2,14 +2,13 @@ <interfaceDefinition> <node name="policy" owner="${vyos_conf_scripts_dir}/policy.py"> <properties> - <priority>470</priority> + <priority>200</priority> <help>Routing policy</help> </properties> <children> <tagNode name="access-list"> <properties> <help>IP access-list filter</help> - <valueHelp> <format>u32:1-99</format> <description>IP standard access list</description> @@ -238,7 +237,6 @@ <tagNode name="extcommunity-list"> <properties> <help>Border Gateway Protocol (BGP) extended community-list filter</help> - <priority>490</priority> <valueHelp> <format>txt</format> <description>Border Gateway Protocol (BGP) extended community-list filter</description> diff --git a/op-mode-definitions/wireguard.xml.in b/op-mode-definitions/wireguard.xml.in index 4aee4b1ac..0df838b50 100644 --- a/op-mode-definitions/wireguard.xml.in +++ b/op-mode-definitions/wireguard.xml.in @@ -26,6 +26,58 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command> </tagNode> + <tagNode name="client-config"> + <properties> + <help>Generate Client config QR code</help> + <completionHelp> + <list><client-name></list> + </completionHelp> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Local interface used for connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> + </completionHelp> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>IP address/FQDN used for client connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + <list><hostname></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv4/IPv6 address used by client</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv4/IPv6 address used by client</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}" --address "${12}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> </children> </node> </children> @@ -73,7 +125,7 @@ <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command> + <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command> <children> <leafNode name="allowed-ips"> <properties> diff --git a/python/vyos/frr.py b/python/vyos/frr.py index de3dbe6e9..df6849472 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -203,7 +203,10 @@ def reload_configuration(config, daemon=None): for i, e in enumerate(output.split('\n')): LOG.debug(f'frr-reload output: {i:3} {e}') if code == 1: - raise CommitError(f'Configuration FRR failed while commiting code, please enabling debugging to examine logs') + raise CommitError('FRR configuration failed while running commit. Please ' \ + 'enable debugging to examine logs.\n\n\n' \ + 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \ + 'and "sudo systemctl stop vyos-configd"') elif code: raise OSError(code, output) diff --git a/python/vyos/ifconfig/tunnel.py b/python/vyos/ifconfig/tunnel.py index 08854a3b0..2a266fc9f 100644 --- a/python/vyos/ifconfig/tunnel.py +++ b/python/vyos/ifconfig/tunnel.py @@ -43,6 +43,7 @@ class TunnelIf(Interface): **{ 'section': 'tunnel', 'prefixes': ['tun',], + 'bridgeable': True, }, } diff --git a/python/vyos/remote.py b/python/vyos/remote.py index ef103f707..3f24d4b33 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import socket import sys import tempfile from ftplib import FTP @@ -23,80 +24,103 @@ import urllib.request from vyos.util import cmd from paramiko import SSHClient + def upload_ftp(local_path, hostname, remote_path,\ - username='anonymous', password='', port=21): + username='anonymous', password='', port=21, source=None): with open(local_path, 'rb') as file: - with FTP() as conn: + with FTP(source_address=source) as conn: conn.connect(hostname, port) conn.login(username, password) conn.storbinary(f'STOR {remote_path}', file) def download_ftp(local_path, hostname, remote_path,\ - username='anonymous', password='', port=21): + username='anonymous', password='', port=21, source=None): with open(local_path, 'wb') as file: - with FTP() as conn: + with FTP(source_address=source) as conn: conn.connect(hostname, port) conn.login(username, password) conn.retrbinary(f'RETR {remote_path}', file.write) def upload_sftp(local_path, hostname, remote_path,\ - username=None, password=None, port=22): + username=None, password=None, port=22, source=None): + sock = None + if source: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((source, 0)) + sock.connect((hostname, port)) with SSHClient() as ssh: ssh.load_system_host_keys() - ssh.connect(hostname, port, username, password) + ssh.connect(hostname, port, username, password, sock=sock) with ssh.open_sftp() as sftp: sftp.put(local_path, remote_path) + if sock: + sock.shutdown() + sock.close() def download_sftp(local_path, hostname, remote_path,\ - username=None, password=None, port=22): + username=None, password=None, port=22, source=None): + sock = None + if source: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind((source, 0)) + sock.connect((hostname, port)) with SSHClient() as ssh: ssh.load_system_host_keys() - ssh.connect(hostname, port, username, password) + ssh.connect(hostname, port, username, password, sock=sock) with ssh.open_sftp() as sftp: sftp.get(remote_path, local_path) + if sock: + sock.shutdown() + sock.close() -def upload_tftp(local_path, hostname, remote_path, port=69): +def upload_tftp(local_path, hostname, remote_path, port=69, source=None): + source_option = f'--interface {source}' if source else '' with open(local_path, 'rb') as file: - cmd(f'curl -s -T - tftp://{hostname}:{port}/{remote_path}', stderr=None, input=file.read()).encode() + cmd(f'curl {source_option} -s -T - tftp://{hostname}:{port}/{remote_path}',\ + stderr=None, input=file.read()).encode() -def download_tftp(local_path, hostname, remote_path, port=69): +def download_tftp(local_path, hostname, remote_path, port=69, source=None): + source_option = f'--interface {source}' if source else '' with open(local_path, 'wb') as file: - file.write(cmd(f'curl -s tftp://{hostname}:{port}/{remote_path}', stderr=None).encode()) + file.write(cmd(f'curl {source_option} -s tftp://{hostname}:{port}/{remote_path}',\ + stderr=None).encode()) def download_http(urlstring, local_path): with open(local_path, 'wb') as file: with urllib.request.urlopen(urlstring) as response: file.write(response.read()) -def download(local_path, urlstring): +def download(local_path, urlstring, source=None): """ Dispatch the appropriate download function for the given URL and save to local path. """ url = urllib.parse.urlparse(urlstring) if url.scheme == 'http' or url.scheme == 'https': + if source: + print("Warning: Custom source address not supported for HTTP connections.", file=sys.stderr) download_http(urlstring, local_path) elif url.scheme == 'ftp': username = url.username if url.username else 'anonymous' - download_ftp(local_path, url.hostname, url.path, username, url.password) + download_ftp(local_path, url.hostname, url.path, username, url.password, source=source) elif url.scheme == 'sftp' or url.scheme == 'scp': - download_sftp(local_path, url.hostname, url.path, url.username, url.password) + download_sftp(local_path, url.hostname, url.path, url.username, url.password, source=source) elif url.scheme == 'tftp': - download_tftp(local_path, url.hostname, url.path) + download_tftp(local_path, url.hostname, url.path, source=source) else: ValueError(f'Unsupported URL scheme: {url.scheme}') -def upload(local_path, urlstring): +def upload(local_path, urlstring, source=None): """ Dispatch the appropriate upload function for the given URL and upload from local path. """ url = urllib.parse.urlparse(urlstring) if url.scheme == 'ftp': username = url.username if url.username else 'anonymous' - upload_ftp(local_path, url.hostname, url.path, username, url.password) + upload_ftp(local_path, url.hostname, url.path, username, url.password, source=source) elif url.scheme == 'sftp' or url.scheme == 'scp': - upload_sftp(local_path, url.hostname, url.path, url.username, url.password) + upload_sftp(local_path, url.hostname, url.path, url.username, url.password, source=source) elif url.scheme == 'tftp': - upload_tftp(local_path, url.hostname, url.path) + upload_tftp(local_path, url.hostname, url.path, source=source) else: ValueError(f'Unsupported URL scheme: {url.scheme}') diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 8ed0f7228..08697eebf 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -143,13 +143,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['local-as', ASN]) def tearDown(self): - self.cli_delete(['policy', 'route-map', route_map_in]) - self.cli_delete(['policy', 'route-map', route_map_out]) - self.cli_delete(['policy', 'prefix-list', prefix_list_in]) - self.cli_delete(['policy', 'prefix-list', prefix_list_out]) - self.cli_delete(['policy', 'prefix-list6', prefix_list_in6]) - self.cli_delete(['policy', 'prefix-list6', prefix_list_out6]) - + self.cli_delete(['policy']) self.cli_delete(base_path) self.cli_commit() @@ -578,7 +572,7 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): verify_families = ['ipv4 unicast', 'ipv6 unicast','ipv4 multicast', 'ipv6 multicast'] flowspec_families = ['address-family ipv4 flowspec', 'address-family ipv6 flowspec'] flowspec_int = 'lo' - + # Per family distance support for family in distance_families: self.cli_set(base_path + ['address-family', family, 'distance', 'external', distance_external]) @@ -590,16 +584,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): if 'ipv6' in family: self.cli_set(base_path + ['address-family', family, 'distance', 'prefix', distance_v6_prefix, 'distance', distance_prefix_value]) - + # IPv4 flowspec interface check self.cli_set(base_path + ['address-family', 'ipv4-flowspec', 'local-install', 'interface', flowspec_int]) - + # IPv6 flowspec interface check self.cli_set(base_path + ['address-family', 'ipv6-flowspec', 'local-install', 'interface', flowspec_int]) - + # Commit changes self.cli_commit() - + # Verify FRR distances configuration frrconfig = self.getFRRconfig(f'router bgp {ASN}') self.assertIn(f'router bgp {ASN}', frrconfig) @@ -610,12 +604,12 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'distance {distance_prefix_value} {distance_v4_prefix}', frrconfig) if 'ipv6' in family: self.assertIn(f'distance {distance_prefix_value} {distance_v6_prefix}', frrconfig) - + # Verify FRR flowspec configuration for family in flowspec_families: self.assertIn(f'{family}', frrconfig) self.assertIn(f'local-install {flowspec_int}', frrconfig) - + def test_bgp_10_vrf_simple(self): router_id = '127.0.0.3' vrfs = ['red', 'green', 'blue'] @@ -640,6 +634,6 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f'router bgp {ASN} vrf {vrf}', frrconfig) self.assertIn(f' bgp router-id {router_id}', frrconfig) - + if __name__ == '__main__': unittest.main(verbosity=2)
\ No newline at end of file diff --git a/src/conf_mode/policy.py b/src/conf_mode/policy.py index f0348fe06..fb732dd81 100755 --- a/src/conf_mode/policy.py +++ b/src/conf_mode/policy.py @@ -25,6 +25,27 @@ from vyos import frr from vyos import airbag airbag.enable() +def routing_policy_find(key, dictionary): + # Recursively traverse a dictionary and extract the value assigned to + # a given key as generator object. This is made for routing policies, + # thus also import/export is checked + for k, v in dictionary.items(): + if k == key: + if isinstance(v, dict): + for a, b in v.items(): + if a in ['import', 'export']: + yield b + else: + yield v + elif isinstance(v, dict): + for result in routing_policy_find(key, v): + yield result + elif isinstance(v, list): + for d in v: + if isinstance(d, dict): + for result in routing_policy_find(key, d): + yield result + def get_config(config=None): if config: conf = config @@ -35,6 +56,15 @@ def get_config(config=None): policy = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + # We also need some additional information from the config, prefix-lists + # and route-maps for instance. They will be used in verify(). + # + # XXX: one MUST always call this without the key_mangling() option! See + # vyos.configverify.verify_common_route_maps() for more information. + tmp = conf.get_config_dict(['protocols'], key_mangling=('-', '_'), + no_tag_node_value_mangle=True) + # Merge policy dict into "regular" config dict + policy = dict_merge(tmp, policy) return policy def verify(policy): @@ -64,20 +94,24 @@ def verify(policy): if policy_type == 'access_list': if 'source' not in rule_config: - raise ConfigError(f'Source {mandatory_error}') + raise ConfigError(f'A source {mandatory_error}') if int(instance) in range(100, 200) or int(instance) in range(2000, 2700): if 'destination' not in rule_config: - raise ConfigError(f'Destination {mandatory_error}') + raise ConfigError(f'A destination {mandatory_error}') if policy_type == 'access_list6': if 'source' not in rule_config: - raise ConfigError(f'Source {mandatory_error}') + raise ConfigError(f'A source {mandatory_error}') if policy_type in ['as_path_list', 'community_list', 'extcommunity_list', 'large_community_list']: if 'regex' not in rule_config: - raise ConfigError(f'Regex {mandatory_error}') + raise ConfigError(f'A regex {mandatory_error}') + + if policy_type in ['prefix_list', 'prefix_list6']: + if 'prefix' not in rule_config: + raise ConfigError(f'A prefix {mandatory_error}') # route-maps tend to be a bit more complex so they get their own verify() section @@ -102,6 +136,37 @@ def verify(policy): if tmp and tmp not in policy.get('large_community_list', []): raise ConfigError(f'large-community-list {tmp} does not exist!') + # Specified prefix-list must exist + tmp = dict_search('match.ip.address.prefix_list', rule_config) + if tmp and tmp not in policy.get('prefix_list', []): + raise ConfigError(f'prefix-list {tmp} does not exist!') + + # Specified prefix-list must exist + tmp = dict_search('match.ipv6.address.prefix_list', rule_config) + if tmp and tmp not in policy.get('prefix_list6', []): + raise ConfigError(f'prefix-list6 {tmp} does not exist!') + + # When routing protocols are active some use prefix-lists, route-maps etc. + # to apply the systems routing policy to the learned or redistributed routes. + # When the "routing policy" changes and policies, route-maps etc. are deleted, + # it is our responsibility to verify that the policy can not be deleted if it + # is used by any routing protocol + if 'protocols' in policy: + for policy_type in ['access_list', 'access_list6', 'as_path_list', 'community_list', + 'extcommunity_list', 'large_community_list', 'prefix_list', 'route_map']: + if policy_type in policy: + for policy_name in list(set(routing_policy_find(policy_type, policy['protocols']))): + found = False + if policy_name in policy[policy_type]: + found = True + # BGP uses prefix-list for selecting both an IPv4 or IPv6 AFI related + # list - we need to go the extra mile here and check both prefix-lists + if policy_type == 'prefix_list' and 'prefix_list6' in policy and policy_name in policy['prefix_list6']: + found = True + if not found: + tmp = policy_type.replace('_','-') + raise ConfigError(f'Can not delete {tmp} "{name}", still in use!') + return None def generate(policy): diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index ac211fb0a..f70f04298 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -139,7 +139,7 @@ def get_leases(config, leases, state, pool=None, sort='ip'): # apply output/display sort if sort == 'ip': - leases = sorted(leases, key = lambda k: int(ip_address(k['ip']))) + leases = sorted(leases, key = lambda k: int(ip_address(k['ip'].split('/')[0]))) else: leases = sorted(leases, key = lambda k: k[sort]) diff --git a/src/op_mode/wireguard_client.py b/src/op_mode/wireguard_client.py new file mode 100755 index 000000000..7a620a01e --- /dev/null +++ b/src/op_mode/wireguard_client.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse +import os + +from jinja2 import Template +from ipaddress import ip_interface + +from vyos.ifconfig import Section +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 +from vyos.util import cmd +from vyos.util import popen + +if os.geteuid() != 0: + exit("You need to have root privileges to run this script.\nPlease try again, this time using 'sudo'. Exiting.") + +server_config = """WireGuard client configuration for interface: {{ interface }} + +To enable this configuration on a VyOS router you can use the following commands: + +=== VyOS (server) configurtation === + +{% for addr in address if address is defined %} +set interfaces wireguard {{ interface }} peer {{ name }} allowed-ips '{{ addr }}' +{% endfor %} +set interfaces wireguard {{ interface }} peer {{ name }} pubkey '{{ pubkey }}' +""" + +client_config = """ +=== RoadWarrior (client) configuration === + +[Interface] +PrivateKey = {{ privkey }} +{% if address is defined and address|length > 0 %} +Address = {{ address | join(', ')}} +{% endif %} +DNS = 1.1.1.1 + +[Peer] +PublicKey = {{ system_pubkey }} +Endpoint = {{ server }}:{{ port }} +AllowedIPs = 0.0.0.0/0, ::/0 + +""" + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-n", "--name", type=str, help='WireGuard peer name', required=True) + parser.add_argument("-i", "--interface", type=str, help='WireGuard interface the client is connecting to', required=True) + parser.add_argument("-s", "--server", type=str, help='WireGuard server IPv4/IPv6 address or FQDN', required=True) + parser.add_argument("-a", "--address", type=str, help='WireGuard client IPv4/IPv6 address', action='append') + args = parser.parse_args() + + interface = args.interface + if interface not in Section.interfaces('wireguard'): + exit(f'WireGuard interface "{interface}" does not exist!') + + wg_pubkey = cmd(f'wg show {interface} | grep "public key"').split(':')[-1].lstrip() + wg_port = cmd(f'wg show {interface} | grep "listening port"').split(':')[-1].lstrip() + + # Generate WireGuard private key + privkey,_ = popen('wg genkey') + # Generate public key portion from given private key + pubkey,_ = popen('wg pubkey', input=privkey) + + config = { + 'name' : args.name, + 'interface' : interface, + 'system_pubkey' : wg_pubkey, + 'privkey': privkey, + 'pubkey' : pubkey, + 'server' : args.server, + 'port' : wg_port, + 'address' : [], + } + + if args.address: + v4_addr = 0 + v6_addr = 0 + for tmp in args.address: + try: + ip = str(ip_interface(tmp).ip) + if is_ipv4(tmp): + config['address'].append(f'{ip}/32') + v4_addr += 1 + elif is_ipv6(tmp): + config['address'].append(f'{ip}/128') + v6_addr += 1 + except: + print(tmp) + exit('Client IP address invalid!') + + if (v4_addr > 1) or (v6_addr > 1): + exit('Client can only have one IPv4 and one IPv6 address.') + + # Clear out terminal first + print('\x1b[2J\x1b[H') + server = Template(server_config, trim_blocks=True).render(config) + print(server) + client = Template(client_config, trim_blocks=True).render(config) + print(client) + qrcode,err = popen('qrencode -t ansiutf8', input=client) + print(qrcode) |