wgctrl-go/client_integration_test.go
Matt Layher 97bc4ad4a1
FreeBSD kernel support (#128)
* 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>
2022-11-04 09:57:56 -04:00

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...))
}