Skip to content

Commit

Permalink
wg-mux-*: fix "wg" commands, add corresponding --ip-cmd for client
Browse files Browse the repository at this point in the history
  • Loading branch information
mk-fg committed Feb 24, 2019
1 parent f5516a5 commit f026f8d
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 30 deletions.
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,13 @@ Same thing as ssh-reverse-mux-\* scripts above, but for negotiating WireGuard
tunnels, with persistent host tunnel IPs tracked via --ident-\* strings with
simple auth via MACs on UDP packets derived from symmetric -s/--auth-secret.

Private key for WireGuard connection is NOT generated by "genkey", but instead
derived from ~/.ssh/id_ed25519 (generated if missing) in a consistent manner
via PBKDF2 (small number of rounds - even 1 should be fine for non-broken hash).

This is done to not bother with keeping and securing an extra secret file with its
own path, and inevitably forgetting about it, leaking it to repo, backups, etc etc.

Client identity, wg port, public key and tunnel IPs are sent in the clear with
relatively weak authentication (hmac of -s/--auth-secret string), but wg server
is also authenticated by pre-shared public key (and --wg-psk, if specified).
Expand Down
47 changes: 22 additions & 25 deletions wg-mux-client
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class WGMuxConfig:
wg_iface = 'wg'
wg_keepalive = 10
key_source_ssh = '~/.ssh/id_ed25519'
key_pbkdf2_rounds = 100 # for ed25519_sk, done on every run
key_pbkdf2_rounds = 10000 # for ed25519_sk, done on every run


err_fmt = lambda err: '[{}] {}'.format(err.__class__.__name__, err)
Expand Down Expand Up @@ -91,9 +91,9 @@ def derive_wg_key(path, ident, auth_secret, person, rounds):
ident_key = hashlib.blake2b(
to_bytes(ident), key=auth_secret, person=person ).digest()
if not path.exists():
sp.run(
['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-C', f'wg-mux.{ident}', '-f', str(path)],
check=True, stdin=sp.DEVNULL )
ident_repr = b64_encode(ident)[:10]
sp.run( ['ssh-keygen', '-q', '-t', 'ed25519', '-N', '', '-C',
f'wg-mux.{ident_repr}', '-f', str(path)], check=True, stdin=sp.DEVNULL )
ed25519_sk = ssh_key_parse(path)
key = bytearray(hashlib.pbkdf2_hmac( 'sha512',
password=ed25519_sk, salt=ident_key,
Expand Down Expand Up @@ -219,8 +219,7 @@ def parse_res(auth_secret, ident, res):
log.debug('Failed to parse/auth response value: {}', err)
return
wg_addr = ipaddress.ip_address(wg_addr)
wg_net = f'{wg_addr}/{wg_mask}'
return wg_addr, wg_net, wg_port
return wg_addr, wg_mask, wg_port


async def mux_negotiate(
Expand Down Expand Up @@ -306,10 +305,14 @@ def main(args=None, conf=None):
' This is required if mux-server is configured to only return last IP octet.')
group.add_argument('--wg-keepalive', metavar='seconds', default=conf.wg_keepalive,
help='WireGuard keepalive interval. Default: %(default)s')
group.add_argument('-c', '--wg-cmd', metavar='cmd', default='wg',
group.add_argument('--wg-cmd', metavar='cmd', default='wg',
help='"wg" command to run, split on spaces.'
' Use e.g. "sudo wg" to have it run via sudo or full path'
' for a special binary with suid/capabilities. Default: %(default)s')
group.add_argument('--ip-cmd', metavar='cmd', default='ip',
help='"ip" command to use for assigning IP address to the interface.'
' Will be run as "ip addr add <addr/mask> dev <wg-iface>".'
' Wrapper can be used to do more stuff. Split on spaces. Default: %(default)s')

group = parser.add_argument_group('Misc options')
group.add_argument('-n', '--attempts',
Expand Down Expand Up @@ -371,9 +374,11 @@ def main(args=None, conf=None):
('af_', sock_af), ('sock_', sock_t), ('ipproto_', sock_p) ]) )
host, port_mux = sock_addr[:2]

wg_cmd = opts.wg_cmd.split()
wg_addr_net = ipaddress.ip_network(opts.wg_net)
wg_cmd, ip_cmd = opts.wg_cmd.split(), opts.ip_cmd.split()
wg_net = ipaddress.ip_network(opts.wg_net)
wg_pk = opts.pubkey
wg_psk = ( opts.wg_psk and 'PresharedKey = '
'{}\n'.format(pl.Path(opts.wg_psk).read_text().strip()) )

wg_key = derive_wg_key(
pl.Path(conf.key_source_ssh).expanduser(),
Expand All @@ -396,25 +401,16 @@ def main(args=None, conf=None):
return

if opts.wg_port: wg_port = opts.wg_port
if not wg_addr_net and not wg_net:
if wg_addr not in wg_net:
print( 'ERROR: mux-server returned address'
' without network, and no --wg-net is specified', file=sys.stderr )
f' outside of specified --wg-net: {wg_addr}', file=sys.stderr )
return 1
elif wg_addr_net:
if isinstance(wg_addr, int): wg_addr = wg_addr_net[wg_addr]
elif wg_addr not in wg_addr_net:
print( 'ERROR: mux-server returned address'
f' outside of specified --wg-net: {wg_addr}', file=sys.stderr )
return 1
wg_net = wg_addr_net
wg_psk = '' if not opts.wg_psk else f'PresharedKey = {opts.wg_psk}\n'
wg_ep = f'{host}:{wg_port}'

log.debug( 'Negotiated wg params:'
' ep={} addr={} pubkey={}', wg_ep, wg_addr, wg_pk )
log.debug( 'Negotiated wg params: ep={} addr={}/{}'
' pubkey={}', wg_ep, wg_addr, wg_mask, wg_pk )
wg_conf = dedent(f'''
[Interface]
Address = {wg_addr}
PrivateKey = {wg_key}
[Peer]
Expand All @@ -423,13 +419,14 @@ def main(args=None, conf=None):
Endpoint = {wg_ep}
PersistentKeepalive = {opts.wg_keepalive}
{wg_psk}''')
cmd = ip_cmd + ['addr', 'add', f'{wg_addr}/{wg_mask}', 'dev', opts.wg_iface]
if not opts.dry_run:
sp.run(wg_cmd + [ 'set', opts.wg_iface,
'peer', wg_pk, 'remove' ], stderr=sp.DEVNULL)
sp.run(wg_cmd + ['setconf', opts.wg_iface,
'/dev/stdin' ], check=True, input=wg_conf.encode())
sp.run(cmd)
else:
log.debug('Config for wg: {}', ' '.join(
log.debug('Config for wg peer: {}', ' '.join(
(line.replace(' ', '') or '//') for line in wg_conf.splitlines() ))
log.debug('Config for wg iface: {}', ' '.join(cmd))

if __name__ == '__main__': sys.exit(main())
9 changes: 4 additions & 5 deletions wg-mux-server
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def main(args=None, conf=None):
import argparse
parser = argparse.ArgumentParser(
description='Multiplexer for WireGuard connections,'
' assigning each one unique IP(s) according to provided ident-sting.')
' assigning each one unique IP(s) according to provided ident-sting.'
' --wg-iface should be pre-configured with listening port,'
' have IP address and in UP state for setup connections to work.')

group = parser.add_argument_group('Bind socket options')
group.add_argument('bind', nargs='?', default='::',
Expand Down Expand Up @@ -311,10 +313,7 @@ def main(args=None, conf=None):
n, wg_addr, ident_repr(ident), req_addr )
cmd = wg_cmd + ['set', opts.wg_iface, 'peer', wg_pk_peer, 'allowed-ips', str(wg_addr)]
if opts.wg_psk: cmd.extend(['preshared-key', opts.wg_psk])
if not opts.dry_run:
sp.run(wg_cmd + [ 'set', opts.wg_iface,
'peer', wg_pk_peer, 'remove' ], stderr=sp.DEVNULL)
sp.run(cmd, check=True)
if not opts.dry_run: sp.run(cmd, check=True)
else: log.debug('Config for peer: {}', ' '.join(cmd))
return wg_addr, wg_net, opts.wg_port

Expand Down

0 comments on commit f026f8d

Please sign in to comment.