diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml
index 78c8042..4d3461d 100644
--- a/.builds/freebsd.yml
+++ b/.builds/freebsd.yml
@@ -1,11 +1,15 @@
image: freebsd/latest
packages:
- go
+ - bash
+ - sudo
+ - wireguard
sources:
- https://github.com/WireGuard/wgctrl-go
environment:
GO111MODULE: "on"
GOBIN: "/home/build/go/bin"
+ CGO_ENABLED: "1"
tasks:
- setup-wireguard: |
./wgctrl-go/.cibuild.sh
diff --git a/.cibuild.sh b/.cibuild.sh
index c5e217a..ae383dd 100755
--- a/.cibuild.sh
+++ b/.cibuild.sh
@@ -15,6 +15,12 @@ if [ "${KERNEL}" == "OpenBSD" ]; then
exit 0
fi
+if [ "${KERNEL}" == "FreeBSD" ]; then
+ # Configure a WireGuard interface.
+ sudo ifconfig wg create name wg0
+ sudo ifconfig wg0 up
+fi
+
if [ "${KERNEL}" == "Linux" ]; then
# Configure a WireGuard interface.
sudo ip link add wg0 type wireguard
diff --git a/README.md b/README.md
index 4295b2f..a08ca38 100644
--- a/README.md
+++ b/README.md
@@ -15,11 +15,15 @@ go get golang.zx2c4.com/wireguard/wgctrl
`wgctrl` can control multiple types of WireGuard devices, including:
-- Linux kernel module devices, via generic netlink
-- userspace devices (e.g. wireguard-go), via the userspace configuration protocol
- - both UNIX-like and Windows operating systems are supported
-- **Experimental:** OpenBSD kernel module devices (read-only), via ioctl interface
- - See for details.
+- Kernel module devices
+ - Linux: via generic netlink
+ - FreeBSD: via ioctl interface
+ - Windows: via ioctl interface
+ - OpenBSD: via ioctl interface (**Experimental**, read-only)
+- Userspace devices via the userspace configuration protocol
+ - [wireguard-go](https://git.zx2c4.com/wireguard-go)
+ - [wireguard-rs](https://git.zx2c4.com/wireguard-rs))
+ - [boringtun](https://github.com/cloudflare/boringtun)
As new operating systems add support for in-kernel WireGuard implementations,
this package should also be extended to support those native implementations.
diff --git a/client_integration_test.go b/client_integration_test.go
index 9df2074..ab4bedc 100644
--- a/client_integration_test.go
+++ b/client_integration_test.go
@@ -1,10 +1,12 @@
package wgctrl_test
import (
+ "bytes"
"errors"
"fmt"
"net"
"os"
+ "sort"
"strings"
"testing"
"time"
@@ -68,7 +70,7 @@ func TestIntegrationClient(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Panic if a specific test takes too long.
- timer := time.AfterFunc(1*time.Minute, func() {
+ timer := time.AfterFunc(5*time.Minute, func() {
panic("test took too long")
})
defer timer.Stop()
@@ -187,6 +189,15 @@ func testConfigure(t *testing.T, c *wgctrl.Client, d *wgtypes.Device) {
}},
}
+ // 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)
}
@@ -354,6 +365,12 @@ func testConfigurePeersUpdateOnly(t *testing.T, c *wgctrl.Client, d *wgtypes.Dev
}
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)
}
diff --git a/internal/wgfreebsd/client_freebsd.go b/internal/wgfreebsd/client_freebsd.go
new file mode 100644
index 0000000..3ae49f9
--- /dev/null
+++ b/internal/wgfreebsd/client_freebsd.go
@@ -0,0 +1,533 @@
+//go:build freebsd
+// +build freebsd
+
+package wgfreebsd
+
+// #include
+// #include
+import "C"
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "net"
+ "os"
+ "runtime"
+ "time"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd/internal/nv"
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd/internal/wgh"
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal"
+ "golang.zx2c4.com/wireguard/wgctrl/wgtypes"
+)
+
+// ifGroupWG is the WireGuard interface group name passed to the kernel.
+var ifGroupWG = [16]byte{0: 'w', 1: 'g'}
+
+var _ wginternal.Client = &Client{}
+
+// A Client provides access to FreeBSD WireGuard ioctl information.
+type Client struct {
+ // Hooks which use system calls by default, but can also be swapped out
+ // during tests.
+ close func() error
+ ioctlIfgroupreq func(*wgh.Ifgroupreq) error
+ ioctlWGDataIO func(uint, *wgh.WGDataIO) error
+}
+
+// New creates a new Client and returns whether or not the ioctl interface
+// is available.
+func New() (*Client, bool, error) {
+ // The FreeBSD ioctl interface operates on a generic AF_INET socket.
+ fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
+ if err != nil {
+ return nil, false, err
+ }
+
+ // TODO(mdlayher): find a call to invoke here to probe for availability.
+ // c.Devices won't work because it returns a "not found" error when the
+ // kernel WireGuard implementation is available but the interface group
+ // has no members.
+
+ // By default, use system call implementations for all hook functions.
+ return &Client{
+ close: func() error { return unix.Close(fd) },
+ ioctlIfgroupreq: ioctlIfgroupreq(fd),
+ ioctlWGDataIO: ioctlWGDataIO(fd),
+ }, true, nil
+}
+
+// Close implements wginternal.Client.
+func (c *Client) Close() error {
+ return c.close()
+}
+
+// Devices implements wginternal.Client.
+func (c *Client) Devices() ([]*wgtypes.Device, error) {
+ ifg := wgh.Ifgroupreq{
+ // Query for devices in the "wg" group.
+ Name: ifGroupWG,
+ }
+
+ // Determine how many device names we must allocate memory for.
+ if err := c.ioctlIfgroupreq(&ifg); err != nil {
+ return nil, err
+ }
+
+ // ifg.Len is size in bytes; allocate enough memory for the correct number
+ // of wgh.Ifgreq and then store a pointer to the memory where the data
+ // should be written (ifgrs) in ifg.Groups.
+ //
+ // From a thread in golang-nuts, this pattern is valid:
+ // "It would be OK to pass a pointer to a struct to ioctl if the struct
+ // contains a pointer to other Go memory, but the struct field must have
+ // pointer type."
+ // See: https://groups.google.com/forum/#!topic/golang-nuts/FfasFTZvU_o.
+ ifgrs := make([]wgh.Ifgreq, ifg.Len/wgh.SizeofIfgreq)
+ ifg.Groups = &ifgrs[0]
+
+ // Now actually fetch the device names.
+ if err := c.ioctlIfgroupreq(&ifg); err != nil {
+ return nil, err
+ }
+
+ // Keep this alive until we're done doing the ioctl dance.
+ runtime.KeepAlive(&ifg)
+
+ devices := make([]*wgtypes.Device, 0, len(ifgrs))
+ for _, ifgr := range ifgrs {
+ // Remove any trailing NULL bytes from the interface names.
+ name := string(bytes.TrimRight(ifgr.Ifgrqu[:], "\x00"))
+
+ device, err := c.Device(name)
+ if err != nil {
+ return nil, err
+ }
+
+ devices = append(devices, device)
+ }
+
+ return devices, nil
+}
+
+// Device implements wginternal.Client.
+func (c *Client) Device(name string) (*wgtypes.Device, error) {
+ dname, err := deviceName(name)
+ if err != nil {
+ return nil, err
+ }
+
+ // First, specify the name of the device and determine how much memory
+ // must be allocated.
+ data := wgh.WGDataIO{
+ Name: dname,
+ }
+
+ var mem []byte
+ for {
+ if err := c.ioctlWGDataIO(wgh.SIOCGWG, &data); err != nil {
+ // ioctl functions always return a wrapped unix.Errno value.
+ // Conform to the wgctrl contract by unwrapping some values:
+ // ENXIO: "no such device": (no such WireGuard device)
+ // EINVAL: "inappropriate ioctl for device" (device is not a
+ // WireGuard device)
+ switch err.(*os.SyscallError).Err {
+ case unix.ENXIO, unix.EINVAL:
+ return nil, os.ErrNotExist
+ default:
+ return nil, err
+ }
+ }
+
+ if len(mem) >= int(data.Size) {
+ // Allocated enough memory!
+ break
+ }
+
+ // Allocate the appropriate amount of memory and point the kernel at
+ // the first byte of our slice's backing array. When the loop continues,
+ // we will check if we've allocated enough memory.
+ mem = make([]byte, data.Size)
+ data.Data = &mem[0]
+ }
+
+ dev, err := parseDevice(mem)
+ if err != nil {
+ return nil, err
+ }
+
+ dev.Name = name
+
+ return dev, nil
+}
+
+// ConfigureDevice implements wginternal.Client.
+func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error {
+ // Check if there is a peer with the UpdateOnly flag set.
+ // This is not supported on FreeBSD yet. So error out..
+ // TODO(stv0g): remove this check once kernel support has landed.
+ for _, peer := range cfg.Peers {
+ if peer.UpdateOnly {
+ // Check that this device is really an existing kernel
+ // device
+ if _, err := c.Device(name); err != os.ErrNotExist {
+ return wgtypes.ErrUpdateOnlyNotSupported
+ }
+ }
+ }
+
+
+ m := unparseConfig(cfg)
+ mem, sz, err := nv.Marshal(m)
+ if err != nil {
+ return err
+ }
+ defer C.free(unsafe.Pointer(mem))
+
+ dname, err := deviceName(name)
+ if err != nil {
+ return err
+ }
+
+ data := wgh.WGDataIO{
+ Name: dname,
+ Data: mem,
+ Size: uint64(sz),
+ }
+
+ if err := c.ioctlWGDataIO(wgh.SIOCSWG, &data); err != nil {
+ // ioctl functions always return a wrapped unix.Errno value.
+ // Conform to the wgctrl contract by unwrapping some values:
+ // ENXIO: "no such device": (no such WireGuard device)
+ // EINVAL: "inappropriate ioctl for device" (device is not a
+ // WireGuard device)
+ switch err.(*os.SyscallError).Err {
+ case unix.ENXIO, unix.EINVAL:
+ return os.ErrNotExist
+ default:
+ return err
+ }
+ }
+
+ return nil
+}
+
+// deviceName converts an interface name string to the format required to pass
+// with wgh.WGGetServ.
+func deviceName(name string) ([16]byte, error) {
+ var out [unix.IFNAMSIZ]byte
+ if len(name) > unix.IFNAMSIZ {
+ return out, fmt.Errorf("wgfreebsd: interface name %q too long", name)
+ }
+
+ copy(out[:], name)
+ return out, nil
+}
+
+// ioctlIfgroupreq returns a function which performs the appropriate ioctl on
+// fd to retrieve members of an interface group.
+func ioctlIfgroupreq(fd int) func(*wgh.Ifgroupreq) error {
+ return func(ifg *wgh.Ifgroupreq) error {
+ return ioctl(fd, unix.SIOCGIFGMEMB, unsafe.Pointer(ifg))
+ }
+}
+
+// ioctlWGDataIO returns a function which performs the appropriate ioctl on
+// fd to issue a WireGuard data I/O.
+func ioctlWGDataIO(fd int) func(uint, *wgh.WGDataIO) error {
+ return func(req uint, data *wgh.WGDataIO) error {
+ return ioctl(fd, req, unsafe.Pointer(data))
+ }
+}
+
+// ioctl is a raw wrapper for the ioctl system call.
+func ioctl(fd int, req uint, arg unsafe.Pointer) error {
+ _, _, errno := unix.Syscall(unix.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))
+ if errno != 0 {
+ return os.NewSyscallError("ioctl", errno)
+ }
+
+ return nil
+}
+
+func panicf(format string, a ...interface{}) {
+ panic(fmt.Sprintf(format, a...))
+}
+
+func ntohs(i uint16) int {
+ b := *(*[2]byte)(unsafe.Pointer(&i))
+ return int(binary.BigEndian.Uint16(b[:]))
+}
+
+func htons(i int) uint16 {
+ b := make([]byte, 2)
+ binary.BigEndian.PutUint16(b, uint16(i))
+ return *(*uint16)(unsafe.Pointer(&b[0]))
+}
+
+// parseEndpoint converts a struct sockaddr to a Go net.UDPAddr
+func parseEndpoint(ep []byte) *net.UDPAddr {
+ sa := (*unix.RawSockaddr)(unsafe.Pointer(&ep[0]))
+
+ switch sa.Family {
+ case unix.AF_INET:
+ sa := (*unix.RawSockaddrInet4)(unsafe.Pointer(&ep[0]))
+
+ ep := &net.UDPAddr{
+ IP: make(net.IP, net.IPv4len),
+ Port: ntohs(sa.Port),
+ }
+ copy(ep.IP, sa.Addr[:])
+
+ return ep
+ case unix.AF_INET6:
+ sa := (*unix.RawSockaddrInet6)(unsafe.Pointer(&ep[0]))
+
+ // TODO(mdlayher): IPv6 zone?
+ ep := &net.UDPAddr{
+ IP: make(net.IP, net.IPv6len),
+ Port: ntohs(sa.Port),
+ }
+ copy(ep.IP, sa.Addr[:])
+
+ return ep
+ default:
+ // No endpoint configured.
+ return nil
+ }
+}
+
+func unparseEndpoint(ep net.UDPAddr) []byte {
+ var b []byte
+
+ if v4 := ep.IP.To4(); v4 != nil {
+ b = make([]byte, unsafe.Sizeof(unix.RawSockaddrInet4{}))
+ sa := (*unix.RawSockaddrInet4)(unsafe.Pointer(&b[0]))
+
+ sa.Family = unix.AF_INET
+ sa.Port = htons(ep.Port)
+ copy(sa.Addr[:], v4)
+ } else if v6 := ep.IP.To16(); v6 != nil {
+ b = make([]byte, unsafe.Sizeof(unix.RawSockaddrInet6{}))
+ sa := (*unix.RawSockaddrInet6)(unsafe.Pointer(&b[0]))
+
+ sa.Family = unix.AF_INET6
+ sa.Port = htons(ep.Port)
+ copy(sa.Addr[:], v6)
+ }
+
+ return b
+}
+
+// parseAllowedIP unpacks a net.IPNet from a WGAIP structure.
+func parseAllowedIP(aip nv.List) net.IPNet {
+ cidr := int(aip["cidr"].(uint64))
+ if ip, ok := aip["ipv4"]; ok {
+ return net.IPNet{
+ IP: net.IP(ip.([]byte)),
+ Mask: net.CIDRMask(cidr, 32),
+ }
+ } else if ip, ok := aip["ipv6"]; ok {
+ return net.IPNet{
+ IP: net.IP(ip.([]byte)),
+ Mask: net.CIDRMask(cidr, 128),
+ }
+ } else {
+ panicf("wgfreebsd: invalid address family for allowed IP: %+v", aip)
+ return net.IPNet{}
+ }
+}
+
+func unparseAllowedIP(aip net.IPNet) nv.List {
+ m := nv.List{}
+
+ ones, _ := aip.Mask.Size()
+ m["cidr"] = uint64(ones)
+
+ if v4 := aip.IP.To4(); v4 != nil {
+ m["ipv4"] = []byte(v4)
+ } else if v6 := aip.IP.To16(); v6 != nil {
+ m["ipv6"] = []byte(v6)
+ }
+
+ return m
+}
+
+// parseTimestamp parses a binary timestamp to a Go time.Time
+func parseTimestamp(b []byte) time.Time {
+ var secs, nsecs int64
+
+ buf := bytes.NewReader(b)
+
+ // TODO(stv0g): Handle non-little endian machines
+ binary.Read(buf, binary.LittleEndian, &secs)
+ binary.Read(buf, binary.LittleEndian, &nsecs)
+
+ if secs == 0 && nsecs == 0 {
+ return time.Time{}
+ }
+
+ return time.Unix(secs, nsecs)
+}
+
+// parsePeer unpacks a wgtypes.Peer from a name-value list (nvlist).
+func parsePeer(v nv.List) wgtypes.Peer {
+ p := wgtypes.Peer{
+ ProtocolVersion: 1,
+ }
+
+ if v, ok := v["public-key"]; ok {
+ pk := (*wgtypes.Key)(v.([]byte))
+ p.PublicKey = *pk
+ }
+
+ if v, ok := v["preshared-key"]; ok {
+ psk := (*wgtypes.Key)(v.([]byte))
+ p.PresharedKey = *psk
+ }
+
+ if v, ok := v["last-handshake-time"]; ok {
+ p.LastHandshakeTime = parseTimestamp(v.([]byte))
+ }
+
+ if v, ok := v["endpoint"]; ok {
+ p.Endpoint = parseEndpoint(v.([]byte))
+ }
+
+ if v, ok := v["persistent-keepalive-interval"]; ok {
+ p.PersistentKeepaliveInterval = time.Second * time.Duration(v.(uint64))
+ }
+
+ if v, ok := v["rx-bytes"]; ok {
+ p.ReceiveBytes = int64(v.(uint64))
+ }
+
+ if v, ok := v["tx-bytes"]; ok {
+ p.ReceiveBytes = int64(v.(uint64))
+ }
+
+ if v, ok := v["allowed-ips"]; ok {
+ m := v.([]nv.List)
+ for _, aip := range m {
+ p.AllowedIPs = append(p.AllowedIPs, parseAllowedIP(aip))
+ }
+ }
+
+ return p
+}
+
+// parseDevice decodes the device from a FreeBSD name-value list (nvlist)
+func parseDevice(data []byte) (*wgtypes.Device, error) {
+ dev := &wgtypes.Device{
+ Type: wgtypes.FreeBSDKernel,
+ }
+
+ m := nv.List{}
+ if err := nv.Unmarshal(data, m); err != nil {
+ return nil, err
+ }
+
+ if v, ok := m["public-key"]; ok {
+ pk := (*wgtypes.Key)(v.([]byte))
+ dev.PublicKey = *pk
+ }
+
+ if v, ok := m["private-key"]; ok {
+ sk := (*wgtypes.Key)(v.([]byte))
+ dev.PrivateKey = *sk
+ }
+
+ if v, ok := m["user-cookie"]; ok {
+ dev.FirewallMark = int(v.(uint64))
+ }
+
+ if v, ok := m["listen-port"]; ok {
+ dev.ListenPort = int(v.(uint64))
+ }
+
+ if v, ok := m["peers"]; ok {
+ m := v.([]nv.List)
+ for _, n := range m {
+ peer := parsePeer(n)
+ dev.Peers = append(dev.Peers, peer)
+ }
+ }
+
+ return dev, nil
+}
+
+// unparsePeerConfig encodes a PeerConfig to a name-value list (nvlist).
+func unparsePeerConfig(cfg wgtypes.PeerConfig) nv.List {
+ m := nv.List{}
+
+ m["public-key"] = cfg.PublicKey[:]
+
+ if v := cfg.PresharedKey; v != nil {
+ m["preshared-key"] = v[:]
+ }
+
+ if v := cfg.PersistentKeepaliveInterval; v != nil {
+ m["persistent-keepalive-interval"] = uint64(v.Seconds())
+ }
+
+ if v := cfg.Endpoint; v != nil {
+ m["endpoint"] = unparseEndpoint(*v)
+ }
+
+ if cfg.ReplaceAllowedIPs {
+ m["replace-allowedips"] = true
+ }
+
+ if cfg.Remove {
+ m["remove"] = true
+ }
+
+ if cfg.AllowedIPs != nil {
+ aips := []nv.List{}
+
+ for _, aip := range cfg.AllowedIPs {
+ aips = append(aips, unparseAllowedIP(aip))
+ }
+
+ m["allowed-ips"] = aips
+ }
+
+ return m
+}
+
+// unparseDevice encodes the device configuration as a FreeBSD name-value list (nvlist).
+func unparseConfig(cfg wgtypes.Config) nv.List {
+ m := nv.List{}
+
+ if v := cfg.PrivateKey; v != nil {
+ m["private-key"] = v[:]
+ }
+
+ if v := cfg.ListenPort; v != nil {
+ m["listen-port"] = uint64(*v)
+ }
+
+ if v := cfg.FirewallMark; v != nil {
+ m["user-cookie"] = uint64(*v)
+ }
+
+ if cfg.ReplacePeers {
+ m["replace-peers"] = true
+ }
+
+ if v := cfg.Peers; v != nil {
+ peers := []nv.List{}
+
+ for _, p := range v {
+ peer := unparsePeerConfig(p)
+ peers = append(peers, peer)
+ }
+
+ m["peers"] = peers
+ }
+
+ return m
+}
diff --git a/internal/wgfreebsd/doc.go b/internal/wgfreebsd/doc.go
new file mode 100644
index 0000000..b4e0f14
--- /dev/null
+++ b/internal/wgfreebsd/doc.go
@@ -0,0 +1,6 @@
+// Package wgfreebsd provides internal access to FreeBSD's WireGuard
+// ioctl interface.
+//
+// This package is internal-only and not meant for end users to consume.
+// Please use package wgctrl (an abstraction over this package) instead.
+package wgfreebsd
diff --git a/internal/wgfreebsd/internal/nv/decode.go b/internal/wgfreebsd/internal/nv/decode.go
new file mode 100644
index 0000000..389f899
--- /dev/null
+++ b/internal/wgfreebsd/internal/nv/decode.go
@@ -0,0 +1,71 @@
+//go:build freebsd
+// +build freebsd
+
+package nv
+
+// #cgo LDFLAGS: -lnv
+// #include
+import "C"
+
+import (
+ "unsafe"
+)
+
+// Unmarshal decodes a FreeBSD name-value list (nv(9)) to a Go map
+func Unmarshal(d []byte, out List) error {
+ sz := C.ulong(len(d))
+ dp := unsafe.Pointer(&d[0])
+ nvl := C.nvlist_unpack(dp, sz, 0)
+
+ return unmarshal(nvl, out)
+}
+
+func unmarshal(nvl *C.struct_nvlist, out List) error {
+ // For debugging
+ // C.nvlist_dump(nvl, C.int(os.Stdout.Fd()))
+
+ var cookie unsafe.Pointer
+ for {
+ var typ C.int
+ ckey := C.nvlist_next(nvl, &typ, &cookie)
+ if ckey == nil {
+ break
+ }
+
+ var sz C.size_t
+ var value interface{}
+ switch typ {
+ case C.NV_TYPE_BINARY:
+ v := C.nvlist_get_binary(nvl, ckey, &sz)
+ value = C.GoBytes(v, C.int(sz))
+
+ case C.NV_TYPE_BOOL:
+ value = C.nvlist_get_bool(nvl, ckey)
+
+ case C.NV_TYPE_NUMBER:
+ v := C.nvlist_get_number(nvl, ckey)
+ value = uint64(v)
+
+ case C.NV_TYPE_NVLIST_ARRAY:
+ items := []List{}
+
+ nvlSubListsBuf := C.nvlist_get_nvlist_array(nvl, ckey, &sz)
+ nvlSubLists := unsafe.Slice(nvlSubListsBuf, sz)
+ for _, nvlSubList := range nvlSubLists {
+ item := map[string]interface{}{}
+ if err := unmarshal(nvlSubList, item); err != nil {
+ return err
+ }
+
+ items = append(items, item)
+ }
+
+ value = items
+ }
+
+ name := C.GoString(ckey)
+ out[name] = value
+ }
+
+ return nil
+}
diff --git a/internal/wgfreebsd/internal/nv/doc.go b/internal/wgfreebsd/internal/nv/doc.go
new file mode 100644
index 0000000..9e76aa9
--- /dev/null
+++ b/internal/wgfreebsd/internal/nv/doc.go
@@ -0,0 +1,3 @@
+// Package nv marshals and unmarshals Go maps to/from FreeBSDs nv(9) name/value lists
+// See: https://www.freebsd.org/cgi/man.cgi?query=nv&sektion=9
+package nv
diff --git a/internal/wgfreebsd/internal/nv/encode.go b/internal/wgfreebsd/internal/nv/encode.go
new file mode 100644
index 0000000..e007962
--- /dev/null
+++ b/internal/wgfreebsd/internal/nv/encode.go
@@ -0,0 +1,75 @@
+//go:build freebsd
+// +build freebsd
+
+package nv
+
+/*
+#cgo LDFLAGS: -lnv
+#include
+#include
+
+// For sizeof(*struct nvlist)
+typedef struct nvlist *nvlist_ptr;
+*/
+import "C"
+
+import (
+ "unsafe"
+)
+
+// Marshal encodes a Go map to a FreeBSD name-value list (nv(9))
+func Marshal(m List) (*byte, int, error) {
+ nvl, err := marshal(m)
+ if err != nil {
+ return nil, -1, err
+ }
+
+ // For debugging
+ // C.nvlist_dump(nvl, C.int(os.Stdout.Fd()))
+
+ var sz C.size_t
+ buf := C.nvlist_pack(nvl, &sz)
+
+ return (*byte)(buf), int(sz), nil
+}
+
+func marshal(m List) (nvl *C.struct_nvlist, err error) {
+ nvl = C.nvlist_create(0)
+
+ for key, value := range m {
+ ckey := C.CString(key)
+
+ switch value := value.(type) {
+ case bool:
+ C.nvlist_add_bool(nvl, ckey, C.bool(value))
+
+ case uint64:
+ C.nvlist_add_number(nvl, ckey, C.ulong(value))
+
+ case []byte:
+ sz := len(value)
+ ptr := C.CBytes(value)
+ C.nvlist_add_binary(nvl, ckey, ptr, C.size_t(sz))
+ C.free(ptr)
+
+ case []List:
+ sz := len(value)
+ buf := C.malloc(C.size_t(C.sizeof_nvlist_ptr * sz))
+ items := (*[1<<30 - 1]*C.struct_nvlist)(buf)
+
+ for i, val := range value {
+ if items[i], err = marshal(val); err != nil {
+ C.free(unsafe.Pointer(ckey))
+ return nil, err
+ }
+ }
+
+ C.nvlist_add_nvlist_array(nvl, ckey, (**C.struct_nvlist)(buf), C.size_t(sz))
+ C.free(buf)
+ }
+
+ C.free(unsafe.Pointer(ckey))
+ }
+
+ return
+}
diff --git a/internal/wgfreebsd/internal/nv/nvlist_test.go b/internal/wgfreebsd/internal/nv/nvlist_test.go
new file mode 100644
index 0000000..2616778
--- /dev/null
+++ b/internal/wgfreebsd/internal/nv/nvlist_test.go
@@ -0,0 +1,45 @@
+//go:build freebsd
+// +build freebsd
+
+package nv_test
+
+import (
+ "fmt"
+ "testing"
+ "unsafe"
+
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd/internal/nv"
+)
+
+func TestMarshaling(t *testing.T) {
+ m1 := nv.List{
+ "number": uint64(0x1234),
+ "boolean": true,
+ "binary": []byte{0xA, 0xB, 0xC, 0xD},
+ "array_of_nvlists": []nv.List{
+ {
+ "a": uint64(1),
+ },
+ {
+ "b": uint64(2),
+ },
+ },
+ }
+
+ buf, sz, err := nv.Marshal(m1)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %s", err)
+ }
+
+ m2 := nv.List{}
+ buf2 := unsafe.Slice(buf, sz)
+
+ err = nv.Unmarshal(buf2, m2)
+ if err != nil {
+ t.Fatalf("Failed to marshal: %s", err)
+ }
+
+ if fmt.Sprint(m1) != fmt.Sprint(m2) {
+ t.Fatalf("unequal: %+#v != %+#v", m1, m2)
+ }
+}
diff --git a/internal/wgfreebsd/internal/nv/types.go b/internal/wgfreebsd/internal/nv/types.go
new file mode 100644
index 0000000..2ff4df3
--- /dev/null
+++ b/internal/wgfreebsd/internal/nv/types.go
@@ -0,0 +1,6 @@
+//go:build freebsd
+// +build freebsd
+
+package nv
+
+type List map[string]interface{}
diff --git a/internal/wgfreebsd/internal/wgh/defs.go b/internal/wgfreebsd/internal/wgh/defs.go
new file mode 100644
index 0000000..ccd593e
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/defs.go
@@ -0,0 +1,51 @@
+//go:build ignore
+// +build ignore
+
+// TODO(mdlayher): attempt to integrate into x/sys/unix infrastructure.
+
+package wgh
+
+/*
+#include
+#include
+#include
+#include
+#include
+
+struct wg_data_io {
+ char wgd_name[IFNAMSIZ];
+ void *wgd_data;
+ size_t wgd_size;
+};
+
+// This is a copy of ifgroupreq but the union's *ifg_req variant is broken out
+// into an explicit field, and the other variant is omitted and replaced with
+// struct padding to the expected size.
+#undef ifgr_groups
+struct go_ifgroupreq {
+ char ifgr_name[IFNAMSIZ];
+ u_int ifgr_len;
+ char ifgr_pad1[-1 * (4 - sizeof(void*))];
+ struct ifg_req *ifgr_groups;
+ char ifgr_pad2[16 - sizeof(void*)];
+};
+
+#define SIOCSWG _IOWR('i', 210, struct wg_data_io)
+#define SIOCGWG _IOWR('i', 211, struct wg_data_io)
+*/
+import "C"
+
+// Interface group types and constants.
+
+const (
+ SizeofIfgreq = C.sizeof_struct_ifg_req
+
+ SIOCGWG = C.SIOCGWG
+ SIOCSWG = C.SIOCSWG
+)
+
+type Ifgroupreq C.struct_go_ifgroupreq
+
+type Ifgreq C.struct_ifg_req
+
+type WGDataIO C.struct_wg_data_io
diff --git a/internal/wgfreebsd/internal/wgh/defs_freebsd_386.go b/internal/wgfreebsd/internal/wgh/defs_freebsd_386.go
new file mode 100644
index 0000000..3a30fef
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/defs_freebsd_386.go
@@ -0,0 +1,32 @@
+//go:build freebsd && 386
+// +build freebsd,386
+
+// Code generated by cmd/cgo -godefs; DO NOT EDIT.
+// cgo -godefs defs.go
+
+package wgh
+
+const (
+ SizeofIfgreq = 0x10
+
+ SIOCGWG = 0xc01869d3
+ SIOCSWG = 0xc01869d2
+)
+
+type Ifgroupreq struct {
+ Name [16]byte
+ Len uint32
+ Pad1 [0]byte
+ Groups *Ifgreq
+ Pad2 [12]byte
+}
+
+type Ifgreq struct {
+ Ifgrqu [16]byte
+}
+
+type WGDataIO struct {
+ Name [16]byte
+ Data *byte
+ Size uint32
+}
diff --git a/internal/wgfreebsd/internal/wgh/defs_freebsd_amd64.go b/internal/wgfreebsd/internal/wgh/defs_freebsd_amd64.go
new file mode 100644
index 0000000..2b8aaa4
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/defs_freebsd_amd64.go
@@ -0,0 +1,32 @@
+//go:build freebsd && amd64
+// +build freebsd,amd64
+
+// Code generated by cmd/cgo -godefs; DO NOT EDIT.
+// cgo -godefs defs.go
+
+package wgh
+
+const (
+ SizeofIfgreq = 0x10
+
+ SIOCGWG = 0xc02069d3
+ SIOCSWG = 0xc02069d2
+)
+
+type Ifgroupreq struct {
+ Name [16]byte
+ Len uint32
+ Pad1 [4]byte
+ Groups *Ifgreq
+ Pad2 [8]byte
+}
+
+type Ifgreq struct {
+ Ifgrqu [16]byte
+}
+
+type WGDataIO struct {
+ Name [16]byte
+ Data *byte
+ Size uint64
+}
diff --git a/internal/wgfreebsd/internal/wgh/defs_freebsd_arm.go b/internal/wgfreebsd/internal/wgh/defs_freebsd_arm.go
new file mode 100644
index 0000000..764edd6
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/defs_freebsd_arm.go
@@ -0,0 +1,32 @@
+//go:build freebsd && arm
+// +build freebsd,arm
+
+// Code generated by cmd/cgo -godefs; DO NOT EDIT.
+// cgo -godefs defs.go
+
+package wgh
+
+const (
+ SizeofIfgreq = 0x10
+
+ SIOCGWG = 0xc01c69d3
+ SIOCSWG = 0xc01c69d2
+)
+
+type Ifgroupreq struct {
+ Name [16]byte
+ Len uint32
+ Pad1 [0]byte
+ Groups *Ifgreq
+ Pad2 [12]byte
+}
+
+type Ifgreq struct {
+ Ifgrqu [16]byte
+}
+
+type WGDataIO struct {
+ Name [16]byte
+ Data *byte
+ Size uint64
+}
diff --git a/internal/wgfreebsd/internal/wgh/defs_freebsd_arm64.go b/internal/wgfreebsd/internal/wgh/defs_freebsd_arm64.go
new file mode 100644
index 0000000..560dfaf
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/defs_freebsd_arm64.go
@@ -0,0 +1,32 @@
+//go:build freebsd && arm64
+// +build freebsd,arm64
+
+// Code generated by cmd/cgo -godefs; DO NOT EDIT.
+// cgo -godefs defs.go
+
+package wgh
+
+const (
+ SizeofIfgreq = 0x10
+
+ SIOCGWG = 0xc02069d3
+ SIOCSWG = 0xc02069d2
+)
+
+type Ifgroupreq struct {
+ Name [16]byte
+ Len uint32
+ Pad1 [4]byte
+ Groups *Ifgreq
+ Pad2 [8]byte
+}
+
+type Ifgreq struct {
+ Ifgrqu [16]byte
+}
+
+type WGDataIO struct {
+ Name [16]byte
+ Data *byte
+ Size uint64
+}
diff --git a/internal/wgfreebsd/internal/wgh/doc.go b/internal/wgfreebsd/internal/wgh/doc.go
new file mode 100644
index 0000000..9832b77
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/doc.go
@@ -0,0 +1,3 @@
+// Package wgh is an auto-generated package which contains constants and
+// types used to access WireGuard information using ioctl calls.
+package wgh
diff --git a/internal/wgfreebsd/internal/wgh/generate.sh b/internal/wgfreebsd/internal/wgh/generate.sh
new file mode 100644
index 0000000..51d329a
--- /dev/null
+++ b/internal/wgfreebsd/internal/wgh/generate.sh
@@ -0,0 +1,33 @@
+#/bin/sh
+
+set -x
+
+# Fix up generated code.
+gofix()
+{
+ IN=$1
+ OUT=$2
+
+ # Change types that are a nuisance to deal with in Go, use byte for
+ # consistency, and produce gofmt'd output.
+ sed 's/]u*int8/]byte/g' $1 | gofmt -s > $2
+}
+
+echo -e "//+build freebsd,amd64\n" > /tmp/wgamd64.go
+GOARCH=amd64 go tool cgo -godefs defs.go >> /tmp/wgamd64.go
+
+echo -e "//+build freebsd,386\n" > /tmp/wg386.go
+GOARCH=386 go tool cgo -godefs defs.go >> /tmp/wg386.go
+
+echo -e "//+build freebsd,arm64\n" > /tmp/wgarm64.go
+GOARCH=arm64 go tool cgo -godefs defs.go >> /tmp/wgarm64.go
+
+echo -e "//+build freebsd,arm\n" > /tmp/wgarm.go
+GOARCH=arm go tool cgo -godefs defs.go >> /tmp/wgarm.go
+
+gofix /tmp/wgamd64.go defs_freebsd_amd64.go
+gofix /tmp/wg386.go defs_freebsd_386.go
+gofix /tmp/wgarm64.go defs_freebsd_arm64.go
+gofix /tmp/wgarm.go defs_freebsd_arm.go
+
+rm -rf _obj/ /tmp/wg*.go
diff --git a/internal/wguser/conn_unix.go b/internal/wguser/conn_unix.go
index 21220a2..c85d8dd 100644
--- a/internal/wguser/conn_unix.go
+++ b/internal/wguser/conn_unix.go
@@ -5,7 +5,7 @@ package wguser
import (
"errors"
- "io/ioutil"
+ "io/fs"
"net"
"os"
"path/filepath"
@@ -29,7 +29,7 @@ func find() ([]string, error) {
func findUNIXSockets(dirs []string) ([]string, error) {
var socks []string
for _, d := range dirs {
- files, err := ioutil.ReadDir(d)
+ files, err := os.ReadDir(d)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
@@ -39,7 +39,7 @@ func findUNIXSockets(dirs []string) ([]string, error) {
}
for _, f := range files {
- if f.Mode()&os.ModeSocket == 0 {
+ if f.Type()&fs.ModeSocket == 0 {
continue
}
diff --git a/internal/wguser/conn_unix_test.go b/internal/wguser/conn_unix_test.go
index 885c20a..b4a59df 100644
--- a/internal/wguser/conn_unix_test.go
+++ b/internal/wguser/conn_unix_test.go
@@ -4,7 +4,6 @@
package wguser
import (
- "io/ioutil"
"net"
"os"
"path/filepath"
@@ -14,14 +13,14 @@ import (
)
func TestUNIX_findUNIXSockets(t *testing.T) {
- tmp, err := ioutil.TempDir(os.TempDir(), "wireguardcfg-test")
+ tmp, err := os.MkdirTemp(os.TempDir(), "wireguardcfg-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %v", err)
}
defer os.RemoveAll(tmp)
// Create a file which is not a device socket.
- f, err := ioutil.TempFile(tmp, "notwg")
+ f, err := os.CreateTemp(tmp, "notwg")
if err != nil {
t.Fatalf("failed to create temporary file: %v", err)
}
@@ -63,7 +62,7 @@ func testFind(dir string) func() ([]string, error) {
func testListen(t *testing.T, device string) (l net.Listener, dir string, done func()) {
t.Helper()
- tmp, err := ioutil.TempDir(os.TempDir(), "wguser-test")
+ tmp, err := os.MkdirTemp(os.TempDir(), "wguser-test")
if err != nil {
t.Fatalf("failed to create temporary directory: %v", err)
}
diff --git a/os_freebsd.go b/os_freebsd.go
new file mode 100644
index 0000000..4f2b2f9
--- /dev/null
+++ b/os_freebsd.go
@@ -0,0 +1,33 @@
+//go:build freebsd
+// +build freebsd
+
+package wgctrl
+
+import (
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wgfreebsd"
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wginternal"
+ "golang.zx2c4.com/wireguard/wgctrl/internal/wguser"
+)
+
+// newClients configures wginternal.Clients for FreeBSD systems.
+func newClients() ([]wginternal.Client, error) {
+ var clients []wginternal.Client
+
+ // FreeBSD has an in-kernel WireGuard implementation. Determine if it is
+ // available and make use of it if so.
+ kc, ok, err := wgfreebsd.New()
+ if err != nil {
+ return nil, err
+ }
+ if ok {
+ clients = append(clients, kc)
+ }
+
+ uc, err := wguser.New()
+ if err != nil {
+ return nil, err
+ }
+
+ clients = append(clients, uc)
+ return clients, nil
+}
diff --git a/os_userspace.go b/os_userspace.go
index 17604c7..e0e14fc 100644
--- a/os_userspace.go
+++ b/os_userspace.go
@@ -1,5 +1,5 @@
-//go:build !linux && !openbsd && !windows
-// +build !linux,!openbsd,!windows
+//go:build !linux && !openbsd && !windows && !freebsd
+// +build !linux,!openbsd,!windows,!freebsd
package wgctrl
diff --git a/wgtypes/errors.go b/wgtypes/errors.go
new file mode 100644
index 0000000..2546410
--- /dev/null
+++ b/wgtypes/errors.go
@@ -0,0 +1,10 @@
+package wgtypes
+
+import (
+ "errors"
+)
+
+// ErrUpdateOnlyNotSupported is returned due to missing kernel support of
+// the PeerConfig UpdateOnly flag.
+var ErrUpdateOnlyNotSupported = errors.New("the UpdateOnly flag is not supported by this platform")
+
diff --git a/wgtypes/types.go b/wgtypes/types.go
index 32eba08..3b33b54 100644
--- a/wgtypes/types.go
+++ b/wgtypes/types.go
@@ -18,6 +18,7 @@ const (
Unknown DeviceType = iota
LinuxKernel
OpenBSDKernel
+ FreeBSDKernel
WindowsKernel
Userspace
)
@@ -29,6 +30,8 @@ func (dt DeviceType) String() string {
return "Linux kernel"
case OpenBSDKernel:
return "OpenBSD kernel"
+ case FreeBSDKernel:
+ return "FreeBSD kernel"
case WindowsKernel:
return "Windows kernel"
case Userspace: