def parse_key(kind, key): try: key = b64decode(key) if type(key) != bytes: raise ValueError('{} has to be bytes'.format(kind)) elif len(key) != wgctl.util.netlink.WG_KEY_LEN: raise ValueError('{} length has to be {}'.format( kind, wgctl.util.netlink.WG_KEY_LEN)) return key except Exception as e: fatal('could not read {}: {}'.format(kind, e))
def get_config(instance): config_path = '/etc/wireguard/{}.yml'.format(instance) if path.isfile(instance): config_path = instance instance = Path(instance).resolve().stem try: with open(config_path) as stream: config = yaml.load(stream) except FileNotFoundError: fatal('could not read file: {}'.format(config_path)) except yaml.YAMLError: fatal('could not parse configuration') check_config(config) return instance, config
def down(context, instance): instance, config = get_config(instance) if not WireGuard().device_exists(ifname=instance): fatal('tunnel interface is already down.') if 'pre_down' in config['interface']: from wgctl.util.exec import run info('running pre-down commands') for cmd in config['interface']['pre_down']: run(context, cmd) port = config['interface']['listen_port'] ip = IPRoute() ip.link('delete', ifname=instance) nets = [peer.get('allowed_ips', []) for peer in config['peers']] nets = [item for sublist in nets for item in sublist] if '0.0.0.0/0' in nets: try: ip.rule('delete', table=254, FRA_SUPPRESS_PREFIXLEN=0, priority=18000) ip.rule('delete', fwmark=port, fwmask=0, table=port, priority=20000) except: pass ok('tunnel brought down successfully')
def fail(message=None): if message is None: fatal('could not parse configuration') else: fatal('could not parse configuration: {}'.format(message))
def info(context, instance): wg = WireGuard() if not wg.device_exists(instance): fatal('device does not exist') instance, config = get_config(instance) interface = WireGuard().get_device_dict(ifname=instance) interface = interface[instance] fwmark = None if interface['fwmark'] > 0: fwmark = interface['fwmark'] attrs = [ attr('interface:', instance), attr('public key:', format_key(interface['public_key'])), attr('listening port:', interface['listen_port']), attr('fwmark:', fwmark) ] output = print_tunnel(config.get('description', '<no tunnel description>')) output += ''.join([line for line in attrs if line is not None]) for peer in interface.get('peers', []): output += '\n' key = format_key(peer['public_key']) peerconf = [ peerconf for peerconf in config['peers'] if peerconf['public_key'] == key ] description = '<no peer description>' if len(peerconf) > 0 and 'description' in peerconf[0]: description = peerconf[0]['description'] endpoint = None if peer.get('endpoint'): endpoint = '{}:{}'.format(peer['endpoint'][0], peer['endpoint'][1]) allowed_ips = None if peer.get('allowedips'): allowed_ips = ', '.join(peer['allowedips']) if peer['last_handshake_time'].year == 1970: handshake = None else: handshake = timeago.format(peer['last_handshake_time'], datetime.now()) if peer['persistent_keepalive_interval'] > 0: keepalive = f'Every {peer["persistent_keepalive_interval"]}s' else: keepalive = None rx_bytes = peer['rx_bytes'] tx_bytes = peer['tx_bytes'] traffic = rx_bytes + tx_bytes attrs = [ attr('public key:', key, pad=4), attr('endpoint:', endpoint, pad=4), attr('allowed ips:', allowed_ips, pad=4), attr('preshared key?', not all(c == '0' for c in peer['preshared_key'].hex()), pad=4), attr('last handshake:', handshake, pad=4), attr('persistent keepalive:', keepalive, pad=4), attr('transfer:', f'↓ {rx_bytes} B ↑ {tx_bytes} B', rx_bytes + tx_bytes > 0, pad=4) ] output += print_peer(description) output += ''.join([line for line in attrs if line is not None]) print(output)
def up(context, instance): instance, config = get_config(instance) wg = WireGuard() if wg.device_exists(ifname=instance): fatal('tunnel interface is already up.') try: with open(config['interface']['private_key']) as key: config['interface']['private_key'] = key.readline().strip() except Exception: fatal('could not read private key file') port = config['interface']['listen_port'] fwmark = config['interface'].get('fwmark') address, cidr = None, None if config['interface'].get('address') is not None: address, cidr = parse_net(config['interface']['address']) ip = IPRoute() try: ip.link('add', ifname=instance, kind='wireguard') except Exception as e: fatal('could not create device: {}'.format(e)) index = ip.link_lookup(ifname=instance)[0] ip.link('set', index=index, state='up') if address is not None and cidr is not None: ip.addr('add', index=index, address=address, prefixlen=cidr) WireGuard().set_device(ifindex=index, config=config) for peer in config['peers']: if peer.get('allowed_ips'): for aip in peer['allowed_ips']: try: if aip == '0.0.0.0/0': ip.route('add', dst='0.0.0.0/0', oif=index, table=port) ip.rule('add', table=254, FRA_SUPPRESS_PREFIXLEN=0, priority=18000) ip.rule('add', fwmark=port, fwmask=0, table=port, priority=20000) else: ip.route('add', dst=aip, oif=index) except Exception as e: fatal('could not create route: {}'.format(e)) if 'post_up' in config['interface']: from wgctl.util.exec import run info('running post-up commands') for cmd in config['interface']['post_up']: run(context, cmd) ok('tunnel tunnel set up successfully')
def set_device(self, ifname=None, ifindex=None, config={}): msg = wgmsg() msg['cmd'] = WG_CMD_SET_DEVICE msg['version'] = WG_GENL_VERSION if ifname != None: msg['attrs'].append(['WGDEVICE_A_IFNAME', ifname]) elif ifindex != None: msg['attrs'].append(['WGDEVICE_A_IFINDEX', ifindex]) else: raise ValueError('ifname or ifindex are unset') interface = config.get('interface') if interface is None: fatal('configuration is missing "interface" key') port = interface.get('listen_port') pkey = interface.get('private_key') if not all([port, pkey]): fatal('interface must have at least a "private_key" and "listen_port"') fwmark = interface.get('fwmark') peers = config.get('peers') if port != None: msg['attrs'].append(['WGDEVICE_A_LISTEN_PORT', port]) if fwmark != None: msg['attrs'].append(['WGDEVICE_A_FWMARK', fwmark]) if pkey != None: msg['attrs'].append(['WGDEVICE_A_PRIVATE_KEY', parse_key('private_key', pkey)]) if peers is not None: wgpeers = [] for peer in peers: wgpeer = wgmsg.wgpeer() if 'preshared_key' in peer: if len(peer['preshared_key']) != 64: fatal('pre-shared key must be 64 hexadecimal characters') wgpeer['attrs'].append(['WGPEER_A_PRESHARED_KEY', bytearray.fromhex(peer['preshared_key'])]) if 'persistent_keepalive_interval' in peer: try: wgpeer['attrs'].append(['WGPEER_A_PERSISTENT_KEEPALIVE_INTERVAL', int(peer['persistent_keepalive_interval'])]) except ValueError: pass if 'endpoint' in peer: try: host, port = peer['endpoint'].rsplit(':') port = int(port) address = \ struct.pack('H', socket.AF_INET) + \ struct.pack('!H12s', port, socket.inet_aton(host)) addr = wgmsg.wgpeer.sockaddr() addr.setvalue(address) wgpeer['attrs'].append(['WGPEER_A_ENDPOINT', addr]) except ValueError: raise ValueError('peer endpoint is malformed') if 'public_key' in peer: wgpeer['attrs'].append(['WGPEER_A_PUBLIC_KEY', parse_key('public_key', peer['public_key'])]) if 'allowed_ips' in peer: wgips = [] for ip in peer['allowed_ips']: net, cidr = ip.rsplit('/') cidr = int(cidr) wgip = wgmsg.wgpeer.wgallowedip() wgip['attrs'].append(['WGALLOWEDIP_A_FAMILY', AF_INET]) wgip['attrs'].append(['WGALLOWEDIP_A_IPADDR', net]) wgip['attrs'].append(['WGALLOWEDIP_A_CIDR_MASK', cidr]) wgips.append(wgip) wgpeer['attrs'].append(['WGPEER_A_ALLOWEDIPS', wgips]) wgpeers.append(wgpeer) msg['attrs'].append(['WGDEVICE_A_PEERS', wgpeers]) return self.put(msg, msg_type=self.prid, msg_flags=NLM_F_REQUEST)
from os import getuid from wgctl.util.cli import fatal from base64 import b64encode from wgctl.commands import \ version, \ conn, \ status CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help']) @click.group(context_settings=CONTEXT_SETTINGS) @click.pass_context @click.option('--verbose', '-v', is_flag=True, default=False) def main(context, verbose): context.obj = {'verbose': verbose} pass if getuid() > 0: fatal('this should be run as root') main.add_command(version.version) main.add_command(conn.up) main.add_command(conn.down) main.add_command(conn.downup) main.add_command(status.status) main.add_command(status.info)