Wagyu

Wagyu is a user-mode WireGuard endpoint for :gen_tcp and :gen_udp sockets, written in Elixir. It needs no TUN device, root or kernel module: the sockets run on a userspace TCP/IP stack, SmolNet, and Wagyu carries their packets to WireGuard peers over one UDP socket.

Why

A conventional WireGuard setup adds a TUN device to the host and routes host traffic through it. That needs root or a kernel module, and it changes networking for everything on the machine.

Wagyu keeps the tunnel inside the BEAM. Each interface has its own SmolNet stack, a userspace TCP/IP stack, and only sockets opened on that stack use the tunnel. An Elixir application can therefore reach hosts on a WireGuard network without root, a kernel module or a TUN device, and without changing the host's routing or how the rest of the node connects.

How

Requirements

Installation

Add wagyu to your dependencies in mix.exs:

def deps do
[
{:wagyu, "~> 0.2.0"}
]
end

Keys

Keys are raw 32-byte binaries. wg genkey, wg pubkey and wg genpsk print them in base64, so decode those:

local_private_key = Base.decode64!("yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=")

Or generate a key pair in Elixir, and give the public key to the peer as Base.encode64(public_key):

{public_key, local_private_key} = :crypto.generate_key(:ecdh, :x25519)

Start an interface

Wagyu.start_link/1 validates the configuration and starts the interface under its own supervisor:

{:ok, interface} =
Wagyu.start_link(
name: :wg0,
private_key: local_private_key,
listen: %{address: {0, 0, 0, 0}, port: 51820},
stack: [
addresses: [{{10, 13, 0, 2}, 32}],
routes: [{{0, 0, 0, 0}, 0, {10, 13, 0, 1}}],
mtu: 1280
],
peers: [
%{
public_key: remote_public_key,
endpoint: %{address: {192, 0, 2, 1}, port: 51820},
allowed_ips: [{{0, 0, 0, 0}, 0}]
}
]
)

A peer may also have a :preshared_key, the 32-byte key wg genpsk makes, which both sides must configure alike. Wagyu.Config describes every option. An interface's configuration is fixed once it starts; to change it, stop the interface and start it again.

To run it in your own supervision tree instead, list {Wagyu, options} as a child.

Connect through the tunnel with :gen_tcp

Wagyu.stack/1 returns the interface's network stack. Pass it, together with SmolNet's TCP module, in the options of an ordinary :gen_tcp call, and that socket's traffic goes through the tunnel:

{:ok, stack} = Wagyu.stack(:wg0)
options = [
{:tcp_module, SmolNet.Inet.Tcp},
{:smolnet_stack, stack},
:inet,
:binary,
{:active, false}
]
{:ok, socket} = :gen_tcp.connect({10, 13, 0, 1}, 443, options, 5_000)
:ok = :gen_tcp.send(socket, "hello")
{:ok, reply} = :gen_tcp.recv(socket, 0, 5_000)
:ok = :gen_tcp.close(socket)

The options apply to that socket only; the node's other TCP connections are unaffected. For IPv6, use SmolNet.Inet6.Tcp with :inet6. The destination must be reachable through the stack's routes and a peer's allowed_ips.

Send datagrams with :gen_udp

UDP works the same way, with SmolNet's UDP module in the udp_module option:

options = [
{:udp_module, SmolNet.Inet.Udp},
{:smolnet_stack, stack},
:inet,
:binary,
{:active, false}
]
{:ok, socket} = :gen_udp.open(0, options)
:ok = :gen_udp.send(socket, {10, 13, 0, 1}, 53, "query")
{:ok, {_address, _port, reply}} = :gen_udp.recv(socket, 0, 5_000)
:ok = :gen_udp.close(socket)

For IPv6, use SmolNet.Inet6.Udp with :inet6. SmolNet's :gen_udp guide covers active mode, connected sockets and the limits on datagram size.

Connect with :ssl

:ssl runs over the same sockets. Give it SmolNet's TCP module as its transport, in the cb_info option, and the stack as for :gen_tcp; the TLS options go in the same list:

{:ok, _apps} = Application.ensure_all_started(:ssl)
{:ok, stack} = Wagyu.stack(:wg0)
options = [
{:cb_info, {SmolNet.Inet.Tcp, :tcp, :tcp_closed, :tcp_error}},
{:smolnet_stack, stack},
:inet,
:binary,
{:active, false},
{:verify, :verify_peer},
{:cacerts, :public_key.cacerts_get()},
{:server_name_indication, ~c"service.example.com"}
]
{:ok, socket} = :ssl.connect({10, 13, 0, 1}, 443, options, 5_000)
:ok = :ssl.send(socket, "hello")
{:ok, reply} = :ssl.recv(socket, 0, 5_000)
:ok = :ssl.close(socket)

SmolNet's :ssl guide covers servers, upgrading a connected socket, and how errors and timeouts appear.

Inspect and stop an interface

Wagyu.info/1 reports counters and peer state, and Wagyu.stop/1 stops the interface. The Wagyu and Wagyu.Config module documentation covers every option, names and handles, failure and restart behaviour, and the limits on queued work.

License

MIT. See LICENSE.

Working on Wagyu itself? See MAINTAINING.md.