mirror of
https://github.com/WireGuard/wgctrl-go.git
synced 2024-09-21 22:50:48 +00:00
97bc4ad4a1
* internal/freebsd: add initial version of FreeBSD support * internal/wguser: Replace deprecated io/ioutil package with io * internal/freebsd: prepare CI to run tests on FreeBSD * test: sort AllowedIPs before diffing them * test: skip integration test configure_peers_update_only on FreeBSD * test: increase test timeout for slow FreeBSD tests * add FreeBSD support to README Signed-off-by: Steffen Vogel <post@steffenvogel.de> * *: tidy * go.mod: bump dependencies * .builds: try to fix OpenBSD Signed-off-by: Matt Layher <mdlayher@gmail.com> Co-authored-by: Steffen Vogel <post@steffenvogel.de>
465 lines
10 KiB
Go
465 lines
10 KiB
Go
package wgctrl_test
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/mikioh/ipaddr"
|
|
"golang.zx2c4.com/wireguard/wgctrl"
|
|
"golang.zx2c4.com/wireguard/wgctrl/internal/wginternal"
|
|
"golang.zx2c4.com/wireguard/wgctrl/internal/wgtest"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
)
|
|
|
|
func TestIntegrationClient(t *testing.T) {
|
|
c, done := integrationClient(t)
|
|
defer done()
|
|
|
|
devices, err := c.Devices()
|
|
if err != nil {
|
|
// It seems that not all errors returned by UNIX socket dialing
|
|
// conform to os.IsPermission, so for now, be lenient and assume that
|
|
// any error here means that permission was denied.
|
|
t.Skipf("skipping, failed to get devices: %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
fn func(t *testing.T, c *wgctrl.Client, d *wgtypes.Device)
|
|
}{
|
|
{
|
|
name: "get",
|
|
fn: testGet,
|
|
},
|
|
{
|
|
name: "configure",
|
|
fn: testConfigure,
|
|
},
|
|
{
|
|
name: "configure many IPs",
|
|
fn: testConfigureManyIPs,
|
|
},
|
|
{
|
|
name: "configure many peers",
|
|
fn: testConfigureManyPeers,
|
|
},
|
|
{
|
|
name: "configure peers update only",
|
|
fn: testConfigurePeersUpdateOnly,
|
|
},
|
|
{
|
|
name: "reset",
|
|
fn: func(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
// Reset device several times; this used to cause a hang in
|
|
// wireguard-go in late 2018.
|
|
for i := 0; i < 10; i++ {
|
|
resetDevice(t, c, d)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Panic if a specific test takes too long.
|
|
timer := time.AfterFunc(5*time.Minute, func() {
|
|
panic("test took too long")
|
|
})
|
|
defer timer.Stop()
|
|
|
|
for _, d := range devices {
|
|
// Break each device into another sub-test so that individual
|
|
// drivers can be implemented as read-only and skip configuration
|
|
// and reset in tests.
|
|
t.Run(d.Name, func(t *testing.T) {
|
|
tt.fn(t, c, d)
|
|
|
|
// Start with a clean state after each test.
|
|
resetDevice(t, c, d)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIntegrationClientIsNotExist(t *testing.T) {
|
|
c, done := integrationClient(t)
|
|
defer done()
|
|
|
|
if _, err := c.Device("wgnotexist0"); !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatalf("expected is not exist error, but got: %v", err)
|
|
}
|
|
}
|
|
|
|
func integrationClient(t *testing.T) (*wgctrl.Client, func()) {
|
|
t.Helper()
|
|
|
|
const (
|
|
env = "WGCTRL_INTEGRATION"
|
|
confirm = "yesreallydoit"
|
|
)
|
|
|
|
if os.Getenv(env) != confirm {
|
|
t.Skipf("skipping, set '%s=%s to run; DANGER: this will reset and reconfigure any detected WireGuard devices",
|
|
env, confirm)
|
|
}
|
|
|
|
c, err := wgctrl.New()
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
t.Skip("skipping, wgctrl is not available on this system")
|
|
}
|
|
|
|
t.Fatalf("failed to open client: %v", err)
|
|
}
|
|
|
|
return c, func() {
|
|
if err := c.Close(); err != nil {
|
|
t.Fatalf("failed to close client: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func testGet(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
t.Logf("device: %s: %s", d.Name, d.PublicKey.String())
|
|
|
|
dn, err := c.Device(d.Name)
|
|
if err != nil {
|
|
t.Fatalf("failed to get %q: %v", d.Name, err)
|
|
}
|
|
|
|
if diff := cmp.Diff(d, dn); diff != "" {
|
|
t.Fatalf("unexpected Device (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func testConfigure(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
var (
|
|
port = 8888
|
|
ips = []net.IPNet{
|
|
wgtest.MustCIDR("192.0.2.0/32"),
|
|
wgtest.MustCIDR("2001:db8::/128"),
|
|
}
|
|
|
|
priv = wgtest.MustPrivateKey()
|
|
peerKey = wgtest.MustPublicKey()
|
|
)
|
|
|
|
t.Logf("before: %s: %s", d.Name, d.PublicKey.String())
|
|
|
|
cfg := wgtypes.Config{
|
|
PrivateKey: &priv,
|
|
ListenPort: &port,
|
|
ReplacePeers: true,
|
|
Peers: []wgtypes.PeerConfig{{
|
|
PublicKey: peerKey,
|
|
ReplaceAllowedIPs: true,
|
|
AllowedIPs: ips,
|
|
}},
|
|
}
|
|
|
|
tryConfigure(t, c, d.Name, cfg)
|
|
|
|
dn, err := c.Device(d.Name)
|
|
if err != nil {
|
|
t.Fatalf("failed to get %q by name: %v", d.Name, err)
|
|
}
|
|
|
|
// Now that a new configuration has been applied, update our initial
|
|
// device for comparison.
|
|
*d = wgtypes.Device{
|
|
Name: d.Name,
|
|
Type: d.Type,
|
|
PrivateKey: priv,
|
|
PublicKey: priv.PublicKey(),
|
|
ListenPort: port,
|
|
Peers: []wgtypes.Peer{{
|
|
PublicKey: peerKey,
|
|
LastHandshakeTime: time.Time{},
|
|
AllowedIPs: ips,
|
|
ProtocolVersion: 1,
|
|
}},
|
|
}
|
|
|
|
// Sort AllowedIPs as different implementations might return
|
|
// them in different order
|
|
for i := range dn.Peers {
|
|
ips := dn.Peers[i].AllowedIPs
|
|
sort.Slice(ips, func(i, j int) bool {
|
|
return bytes.Compare(ips[i].IP, ips[j].IP) > 0
|
|
})
|
|
}
|
|
|
|
if diff := cmp.Diff(d, dn); diff != "" {
|
|
t.Fatalf("unexpected Device from Device (-want +got):\n%s", diff)
|
|
}
|
|
|
|
// Leading space for alignment.
|
|
out := fmt.Sprintf(" after: %s: %s\n", dn.Name, dn.PublicKey.String())
|
|
for _, p := range dn.Peers {
|
|
out += fmt.Sprintf("- peer: %s, IPs: %s\n", p.PublicKey.String(), ipsString(p.AllowedIPs))
|
|
}
|
|
|
|
t.Log(out)
|
|
}
|
|
|
|
func testConfigureManyIPs(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
// Apply 511 IPs per peer.
|
|
var (
|
|
countIPs int
|
|
peers []wgtypes.PeerConfig
|
|
)
|
|
|
|
for i := 0; i < 2; i++ {
|
|
cidr := "2001:db8::/119"
|
|
if i == 1 {
|
|
cidr = "2001:db8:ffff::/119"
|
|
}
|
|
|
|
cur, err := ipaddr.Parse(cidr)
|
|
if err != nil {
|
|
t.Fatalf("failed to create cursor: %v", err)
|
|
}
|
|
|
|
var ips []net.IPNet
|
|
for pos := cur.Next(); pos != nil; pos = cur.Next() {
|
|
bits := 128
|
|
if pos.IP.To4() != nil {
|
|
bits = 32
|
|
}
|
|
|
|
ips = append(ips, net.IPNet{
|
|
IP: pos.IP,
|
|
Mask: net.CIDRMask(bits, bits),
|
|
})
|
|
}
|
|
|
|
peers = append(peers, wgtypes.PeerConfig{
|
|
PublicKey: wgtest.MustPublicKey(),
|
|
ReplaceAllowedIPs: true,
|
|
AllowedIPs: ips,
|
|
})
|
|
|
|
countIPs += len(ips)
|
|
}
|
|
|
|
cfg := wgtypes.Config{
|
|
ReplacePeers: true,
|
|
Peers: peers,
|
|
}
|
|
|
|
tryConfigure(t, c, d.Name, cfg)
|
|
|
|
dn, err := c.Device(d.Name)
|
|
if err != nil {
|
|
t.Fatalf("failed to get %q by name: %v", d.Name, err)
|
|
}
|
|
|
|
peerIPs := countPeerIPs(dn)
|
|
if diff := cmp.Diff(countIPs, peerIPs); diff != "" {
|
|
t.Fatalf("unexpected number of configured peer IPs (-want +got):\n%s", diff)
|
|
}
|
|
|
|
t.Logf("device: %s: %d IPs", d.Name, peerIPs)
|
|
}
|
|
|
|
func testConfigureManyPeers(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
const (
|
|
nPeers = 256
|
|
peerIPs = 512
|
|
)
|
|
|
|
var peers []wgtypes.PeerConfig
|
|
for i := 0; i < nPeers; i++ {
|
|
var (
|
|
pk = wgtest.MustPresharedKey()
|
|
dur = 10 * time.Second
|
|
)
|
|
|
|
ips := generateIPs((i + 1) * 2)
|
|
|
|
peers = append(peers, wgtypes.PeerConfig{
|
|
PublicKey: wgtest.MustPublicKey(),
|
|
PresharedKey: &pk,
|
|
ReplaceAllowedIPs: true,
|
|
Endpoint: &net.UDPAddr{
|
|
IP: ips[0].IP,
|
|
Port: 1111,
|
|
},
|
|
PersistentKeepaliveInterval: &dur,
|
|
AllowedIPs: ips,
|
|
})
|
|
}
|
|
|
|
var (
|
|
priv = wgtest.MustPrivateKey()
|
|
n = 0
|
|
)
|
|
|
|
cfg := wgtypes.Config{
|
|
PrivateKey: &priv,
|
|
ListenPort: &n,
|
|
FirewallMark: &n,
|
|
ReplacePeers: true,
|
|
Peers: peers,
|
|
}
|
|
|
|
tryConfigure(t, c, d.Name, cfg)
|
|
|
|
dn, err := c.Device(d.Name)
|
|
if err != nil {
|
|
t.Fatalf("failed to get updated device: %v", err)
|
|
}
|
|
|
|
if diff := cmp.Diff(nPeers, len(dn.Peers)); diff != "" {
|
|
t.Fatalf("unexpected number of peers (-want +got):\n%s", diff)
|
|
}
|
|
|
|
countIPs := countPeerIPs(dn)
|
|
if diff := cmp.Diff(peerIPs, countIPs); diff != "" {
|
|
t.Fatalf("unexpected number of peer IPs (-want +got):\n%s", diff)
|
|
}
|
|
|
|
t.Logf("device: %s: %d peers, %d IPs", d.Name, len(dn.Peers), countIPs)
|
|
}
|
|
|
|
func testConfigurePeersUpdateOnly(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
var (
|
|
peerA = wgtest.MustPublicKey()
|
|
peerB = wgtest.MustPublicKey()
|
|
psk = wgtest.MustPresharedKey()
|
|
)
|
|
|
|
// Create an initial peer configuration.
|
|
cfg := wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{{
|
|
PublicKey: peerA,
|
|
}},
|
|
}
|
|
|
|
tryConfigure(t, c, d.Name, cfg)
|
|
|
|
// Create an updated configuration that should only apply to the existing
|
|
// peer due to update only flags.
|
|
cfg = wgtypes.Config{
|
|
Peers: []wgtypes.PeerConfig{
|
|
{
|
|
PublicKey: peerA,
|
|
UpdateOnly: true,
|
|
PresharedKey: &psk,
|
|
},
|
|
{
|
|
PublicKey: peerB,
|
|
UpdateOnly: true,
|
|
PresharedKey: &psk,
|
|
},
|
|
},
|
|
}
|
|
|
|
if err := c.ConfigureDevice(d.Name, cfg); err != nil {
|
|
if d.Type == wgtypes.FreeBSDKernel && err == wgtypes.ErrUpdateOnlyNotSupported {
|
|
// TODO(stv0g): remove as soon as the FreeBSD kernel module supports it
|
|
t.Skip("FreeBSD kernel devices do not support UpdateOnly flag")
|
|
}
|
|
|
|
|
|
t.Fatalf("failed to configure second time on %q: %v", d.Name, err)
|
|
}
|
|
|
|
dn, err := c.Device(d.Name)
|
|
if err != nil {
|
|
t.Fatalf("failed to get updated device: %v", err)
|
|
}
|
|
|
|
want := []wgtypes.Peer{{
|
|
PublicKey: peerA,
|
|
PresharedKey: psk,
|
|
ProtocolVersion: 1,
|
|
}}
|
|
|
|
if diff := cmp.Diff(want, dn.Peers); diff != "" {
|
|
t.Fatalf("unexpected configured peers (-want +got):\n%s", diff)
|
|
}
|
|
}
|
|
|
|
func resetDevice(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
|
|
t.Helper()
|
|
|
|
zero := 0
|
|
cfg := wgtypes.Config{
|
|
// Clear device config.
|
|
PrivateKey: &wgtypes.Key{},
|
|
ListenPort: &zero,
|
|
FirewallMark: &zero,
|
|
|
|
// Clear all peers.
|
|
ReplacePeers: true,
|
|
}
|
|
|
|
tryConfigure(t, c, d.Name, cfg)
|
|
}
|
|
|
|
func tryConfigure(t *testing.T, c *wgctrl.Client, device string, cfg wgtypes.Config) {
|
|
t.Helper()
|
|
|
|
if err := c.ConfigureDevice(device, cfg); err != nil {
|
|
if err == wginternal.ErrReadOnly {
|
|
t.Skipf("skipping, device %q implementation is read-only", device)
|
|
}
|
|
|
|
t.Fatalf("failed to configure %q: %v", device, err)
|
|
}
|
|
}
|
|
|
|
func countPeerIPs(d *wgtypes.Device) int {
|
|
var count int
|
|
for _, p := range d.Peers {
|
|
count += len(p.AllowedIPs)
|
|
}
|
|
|
|
return count
|
|
}
|
|
|
|
func ipsString(ipns []net.IPNet) string {
|
|
ss := make([]string, 0, len(ipns))
|
|
for _, ipn := range ipns {
|
|
ss = append(ss, ipn.String())
|
|
}
|
|
|
|
return strings.Join(ss, ", ")
|
|
}
|
|
|
|
func generateIPs(n int) []net.IPNet {
|
|
cur, err := ipaddr.Parse("2001:db8::/64")
|
|
if err != nil {
|
|
panicf("failed to create cursor: %v", err)
|
|
}
|
|
|
|
ips := make([]net.IPNet, 0, n)
|
|
for i := 0; i < n; i++ {
|
|
pos := cur.Next()
|
|
if pos == nil {
|
|
panic("hit nil IP during IP generation")
|
|
}
|
|
|
|
ips = append(ips, net.IPNet{
|
|
IP: pos.IP,
|
|
Mask: net.CIDRMask(128, 128),
|
|
})
|
|
}
|
|
|
|
return ips
|
|
}
|
|
|
|
func panicf(format string, a ...interface{}) {
|
|
panic(fmt.Sprintf(format, a...))
|
|
}
|