Skip to content

Commit

Permalink
Add ifname mode and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
leomos committed Apr 8, 2024
1 parent ba467fb commit 5789359
Show file tree
Hide file tree
Showing 22 changed files with 1,374 additions and 213 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dwgd
.vagrant
.vscode
136 changes: 90 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,103 @@
# dwgd: Docker WireGuard Driver

**dwgd** is a Docker plugin that let your containers connect to a WireGuard network.
This is achieved by [moving a WireGuard network interface](https://www.wireguard.com/netns/) from `dwgd` running namespace into the designated container namespace.
**dwgd** is a Docker plugin that let your containers connect to a WireGuard
network.
This is achieved by [moving a WireGuard network interface](https://www.wireguard.com/netns/)
from `dwgd` running namespace into the designated container namespace.

**Credits**: this is a rewrite of the proof of concept presented in [this great article](https://www.bestov.io/blog/using-wireguard-as-the-network-for-a-docker-container).
## Usage

### Disclaimer
### 1. Start the daemon

This software is by no means ready for production.
I use it in personal projects, but it has not been tested anywhere else other than my machine (as far as I know).
Use it at your own (high) risk.
Start the `dwgd` daemon:
```
$ sudo dwgd -d /var/lib/dwgd.db
[...]
```

Also it misses tests and documentation...eventually I'll write them, I swear.
### 2. Create the docker network

Depending on which [driver specific options](https://docs.docker.com/reference/cli/docker/network/create/#options)
(`-o`) you pass during the network creation phase, you can select two modes:

- [ifname mode](#ifname-mode): if you want to connect to a WireGuard
interface that's living in the **same host** as the one you are running your
containers on;

- [pubkey mode](#pubkey-mode): if you want to connect to a WireGuard interface
that's living in a **different host** as the one you are running your
containers on.

## Usage

Generate the public key given your seed and the IP address that your container will have:
#### Ifname mode

In this mode, the name of a **local** WireGuard interface is passed as an option.
`dwgd` will create a new WireGuard interface that will peer to the one you
passed and hand it to the containers.

Options that you need to pass:

- `dwgd.ifname`: the name of the **local** interface;
- `dwgd.seed`: secret seed that will be used to generate public and private keys
by SHA256 hashing the `{IP, seed}` couple.

```
$ dwgd pubkey -s supersecretseed -i 10.0.0.2
oKetpvdq/I/c7hTW6/AtQPqVlSzgx3q2ClWCx/OXS00=
docker network create \
--driver=dwgd \
-o dwgd.ifname=wg0 \
-o dwgd.seed=supersecretseed \
--subnet=10.0.0.0/24 \
--gateway=10.0.0.1 \
dwgd-net
```

Start dwgd:
#### Pubkey mode

In this mode, an endpoint and a public key for a WireGuard peer to which
containers should connect to are passed as arguments.


**Note**

Please note that you will likely need to modify manually the configuration of
the remote WireGuard peer by adding each container as a peer.

This is doable because public and private keys are deterministically generated
by hashing the `{IP, seed}` couple.

You can generate the public key for an `{IP, seed}` couple using the following
command:

```
$ sudo dwgd -d /var/lib/dwgd.db
[...]
$ dwgd pubkey -s supersecretseed -i 10.0.0.2
oKetpvdq/I/c7hTW6/AtQPqVlSzgx3q2ClWCx/OXS00=
```

Create the docker network with the same seed you used above:
Options that you need to pass:

- `dwgd.pubkey`: the public key of the remote WireGuard interface;
- `dwgd.seed`: secret seed that will be used to generate public and private keys
by SHA256 hashing the `{IP, seed}` couple;
- `dwgd.endpoint`: the endpoint of the WireGuard peer you want your docker
containers to connect to.

Create the docker network with the same seed you used to generate the public
key:
```
$ docker network create \
--driver=dwgd \
-o dwgd.endpoint=example.com:51820 \
-o dwgd.seed=supersecretseed \
-o dwgd.pubkey="your server's public key" \
-o dwgd.pubkey="your remote WireGuard peer's public key" \
--subnet=10.0.0.0/24 \
--gateway=10.0.0.1 \
dwgd_net
```

Start a docker container with the network you just created:
### 3. Start a container

Note that the IP must be set manually.

```
$ docker run -it --rm --network=dwgd_net --ip=10.0.0.2 busybox
/ # ip a
Expand All @@ -64,43 +122,29 @@ rtt min/avg/max/mdev = 8.343/8.990/9.976/0.708 ms

## Installation

So far it has been tested in a Linux machine with Ubuntu 20.04, but I guess it could work on any reasonably recent Linux system that respects the dependencies.
This software has been tested in a Linux machine with Debian 12, but I guess it
could work on any reasonably recent Linux system that respects the dependencies.

After cloning the repository you can build the binary and optionally install the systemd unit.
After cloning the repository you can build the binary and optionally install
the systemd unit.
```
$ go build -o /usr/bin/dwgd ./cmd/dwgd.go
$ chmod +x /usr/bin/dwgd
$ install init/* /etc/systemd/system/
$ install systemd/* /etc/systemd/system/
```

### Dependencies

You need to have WireGuard installed on your system and the `iproute2` package: `dwgd` uses the `ip` command to create and delete the WireGuard interfaces.
You need to have WireGuard installed on your system and the `iproute2` package:
`dwgd` uses the `ip` command to create and delete the WireGuard interfaces.

You will also need the `nsenter` binary if you want `dwgd` to work with docker rootless.
You will also need the `nsenter` binary if you want `dwgd` to work with docker
rootless.

## Development

You can develop on your own machine by compiling `dwgd`, creating a WireGuard network and starting `dwgd`:

```sh
go build ./cmd/dwgd.go
# create server keys
SERVER_PRIVATE_KEY=$(wg genkey)
SERVER_PUBLIC_KEY=$(echo $SERVER_PRIVATE_KEY | wg pubkey)
# create new dwgd0 wireguard interface
sudo ip link add dwgd0 type wireguard
echo $SERVER_PRIVATE_KEY | sudo wg set dwgd0 private-key /dev/fd/0 listen-port 51820
sudo ip address add 10.0.0.1/24 dev dwgd0
# bring interface up
sudo ip link set up dev dwgd0
# generate your container's public key with a specific seed
CLIENT_PUBLIC_KEY=$(./dwgd pubkey -i 10.0.0.2 -s supersecretseed)
sudo wg set dwgd0 peer $CLIENT_PUBLIC_KEY allowed-ips 10.0.0.2/32
# run dwgd driver
sudo ./dwgd -v &
# create docker network with the previously set server public key and seed
docker network create --driver=dwgd -o dwgd.endpoint=localhost:51820 -o dwgd.seed=supersecretseed -o dwgd.pubkey=$SERVER_PUBLIC_KEY --subnet="10.0.0.0/24" --gateway=10.0.0.1 dwgd-net
# run your container
docker run -it --rm --network=dwgd-net --ip=10.0.0.2 busybox
```
Please refer to [the development directory](development/README.md).

## Credits

This is a rewrite of the proof of concept presented in [this great article](https://www.bestov.io/blog/using-wireguard-as-the-network-for-a-docker-container).
10 changes: 4 additions & 6 deletions cmd/dwgd.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package main

import (
"dwgd"
"flag"
"net"
"os"
"os/signal"
"syscall"

"github.com/leomos/dwgd"
)

var Version string
Expand Down Expand Up @@ -41,11 +42,8 @@ func pubkey(args []string) {
os.Exit(1)
}

privkey, err := dwgd.GeneratePrivateKey([]byte(seed), net.ParseIP(ip))
if err != nil {
dwgd.EventsLog.Printf("Couldn't generate key: %s\n", err)
os.Exit(1)
}
privkey := dwgd.GeneratePrivateKey([]byte(seed), net.ParseIP(ip))

dwgd.EventsLog.Printf("%s\n", privkey.PublicKey().String())
os.Exit(0)
}
Expand Down
57 changes: 57 additions & 0 deletions commander.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package dwgd

import (
"io/fs"
"os"
"os/exec"
)

// commander abstracts the os and os/exec stdlib packages.
// This is needed to mock in unit tests.
type commander interface {
// os
Chmod(name string, mode fs.FileMode) error
MkdirAll(name string, perm fs.FileMode) error
ReadFile(name string) ([]byte, error)
ReadDir(name string) ([]fs.DirEntry, error)
Remove(name string) error
Symlink(oldname string, newname string) error
// os/exec
LookPath(file string) (string, error)
Run(name string, arg ...string) error
}

type execCommander struct{}

func (e *execCommander) Chmod(name string, mode fs.FileMode) error {
return os.Chmod(name, mode)
}

func (e *execCommander) MkdirAll(path string, perm fs.FileMode) error {
return os.MkdirAll(path, perm)
}

func (e *execCommander) ReadDir(name string) ([]fs.DirEntry, error) {
return os.ReadDir(name)
}

func (e *execCommander) ReadFile(name string) ([]byte, error) {
return os.ReadFile(name)
}

func (e *execCommander) Remove(name string) error {
return os.Remove(name)
}

func (e *execCommander) Symlink(oldname string, newname string) error {
return os.Symlink(oldname, newname)
}

func (e *execCommander) LookPath(file string) (string, error) {
return exec.LookPath(file)
}

func (e *execCommander) Run(name string, arg ...string) error {
cmd := exec.Command(name, arg...)
return cmd.Run()
}
7 changes: 4 additions & 3 deletions config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package dwgd

// A Config represents the configuration of an instance of a dwgd driver.
type Config struct {
Db string
Verbose bool
Rootless bool
Db string // path to the database
Verbose bool // whether to print debug logs or not
Rootless bool // whether to run in rootless compatibility mode or not
}

func NewConfig() *Config {
Expand Down
35 changes: 35 additions & 0 deletions development/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Development

This folder contains some utilities that can aid the development of `dwgd`:
- `e2e-tests/` contains tests that check the whole `dwgd` lifecycle by creating
the necessary resources (WireGuard interface and docker network), sending a ping
from the container and finally removing everything. The tests can be run like:
`sudo ./development/e2e-tests/test_ifname_mode.sh` and everything should be `OK`.
- `Vagrantfile` a simple Vagrant box that has everything it's needed to run the
`e2e-tests`.

## Developing on local machine

You can develop on your own machine by compiling `dwgd`, creating a WireGuard network and starting `dwgd`:

```sh
go build ./cmd/dwgd.go
# create server keys
SERVER_PRIVATE_KEY=$(wg genkey)
SERVER_PUBLIC_KEY=$(echo $SERVER_PRIVATE_KEY | wg pubkey)
# create new dwgd0 wireguard interface
sudo ip link add dwgd0 type wireguard
echo $SERVER_PRIVATE_KEY | sudo wg set dwgd0 private-key /dev/fd/0 listen-port 51820
sudo ip address add 10.0.0.1/24 dev dwgd0
# bring interface up
sudo ip link set up dev dwgd0
# generate your container's public key with a specific seed
CLIENT_PUBLIC_KEY=$(./dwgd pubkey -i 10.0.0.2 -s supersecretseed)
sudo wg set dwgd0 peer $CLIENT_PUBLIC_KEY allowed-ips 10.0.0.2/32
# run dwgd driver
sudo ./dwgd -v &
# create docker network with the previously set server public key and seed
docker network create --driver=dwgd -o dwgd.endpoint=localhost:51820 -o dwgd.seed=supersecretseed -o dwgd.pubkey=$SERVER_PUBLIC_KEY --subnet="10.0.0.0/24" --gateway=10.0.0.1 dwgd-net
# run your container
docker run -it --rm --network=dwgd-net --ip=10.0.0.2 busybox
```
14 changes: 14 additions & 0 deletions development/Vagrantfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
config.vm.box = "debian/bookworm64"

config.vm.hostname = "dwgd-box"

config.vm.provision "shell", inline: <<-SHELL
apt-get update
apt-get install -y docker.io wireguard
usermod -aG docker vagrant
SHELL
end
Loading

0 comments on commit 5789359

Please sign in to comment.