From b76f2e2af4b609f72072dab5d16dcc90f214c149 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sun, 9 Oct 2022 20:06:48 +0200 Subject: [PATCH 1/7] internal/freebsd: add initial version of FreeBSD support Signed-off-by: Steffen Vogel --- internal/wgfreebsd/client_freebsd.go | 519 ++++++++++++++++++ internal/wgfreebsd/doc.go | 6 + internal/wgfreebsd/internal/nv/decode.go | 71 +++ internal/wgfreebsd/internal/nv/doc.go | 3 + internal/wgfreebsd/internal/nv/encode.go | 75 +++ internal/wgfreebsd/internal/nv/nvlist_test.go | 45 ++ internal/wgfreebsd/internal/nv/types.go | 6 + internal/wgfreebsd/internal/wgh/defs.go | 51 ++ .../internal/wgh/defs_freebsd_386.go | 32 ++ .../internal/wgh/defs_freebsd_amd64.go | 32 ++ .../internal/wgh/defs_freebsd_arm.go | 32 ++ .../internal/wgh/defs_freebsd_arm64.go | 32 ++ internal/wgfreebsd/internal/wgh/doc.go | 3 + internal/wgfreebsd/internal/wgh/generate.sh | 33 ++ os_freebsd.go | 33 ++ os_userspace.go | 4 +- wgtypes/types.go | 3 + 17 files changed, 978 insertions(+), 2 deletions(-) create mode 100644 internal/wgfreebsd/client_freebsd.go create mode 100644 internal/wgfreebsd/doc.go create mode 100644 internal/wgfreebsd/internal/nv/decode.go create mode 100644 internal/wgfreebsd/internal/nv/doc.go create mode 100644 internal/wgfreebsd/internal/nv/encode.go create mode 100644 internal/wgfreebsd/internal/nv/nvlist_test.go create mode 100644 internal/wgfreebsd/internal/nv/types.go create mode 100644 internal/wgfreebsd/internal/wgh/defs.go create mode 100644 internal/wgfreebsd/internal/wgh/defs_freebsd_386.go create mode 100644 internal/wgfreebsd/internal/wgh/defs_freebsd_amd64.go create mode 100644 internal/wgfreebsd/internal/wgh/defs_freebsd_arm.go create mode 100644 internal/wgfreebsd/internal/wgh/defs_freebsd_arm64.go create mode 100644 internal/wgfreebsd/internal/wgh/doc.go create mode 100644 internal/wgfreebsd/internal/wgh/generate.sh create mode 100644 os_freebsd.go diff --git a/internal/wgfreebsd/client_freebsd.go b/internal/wgfreebsd/client_freebsd.go new file mode 100644 index 0000000..fa55a3a --- /dev/null +++ b/internal/wgfreebsd/client_freebsd.go @@ -0,0 +1,519 @@ +//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 { + 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/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/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: From f83f6484a4a211ac09a0283df6729d5054051ada Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Mon, 10 Oct 2022 01:24:50 +0200 Subject: [PATCH 2/7] internal/wguser: Replace deprecated io/ioutil package with io Signed-off-by: Steffen Vogel --- internal/wguser/conn_unix.go | 6 +++--- internal/wguser/conn_unix_test.go | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) 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) } From cb9dd8dda3a31747d8e9e2a0d15aa8a90b9c2ce5 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Mon, 10 Oct 2022 06:55:07 +0200 Subject: [PATCH 3/7] internal/freebsd: prepare CI to run tests on FreeBSD Signed-off-by: Steffen Vogel --- .builds/freebsd.yml | 4 ++++ .cibuild.sh | 6 ++++++ 2 files changed, 10 insertions(+) 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 From 83aad268fd3b328619dbcd21119c8c586710f590 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sun, 16 Oct 2022 11:14:35 +0200 Subject: [PATCH 4/7] test: sort AllowedIPs before diffing them At least the FreeBSD kernel seems to return the AllowedIPs in a different order than the others. Signed-off-by: Steffen Vogel --- client_integration_test.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/client_integration_test.go b/client_integration_test.go index 9df2074..c9bf0e0 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" @@ -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) } From 846054fa57c4fac96c9063e8723a139e5d3c55b5 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Tue, 11 Oct 2022 21:36:53 +0200 Subject: [PATCH 5/7] test: skip integration test configure_peers_update_only on FreeBSD Signed-off-by: Steffen Vogel --- client_integration_test.go | 6 ++++++ internal/wgfreebsd/client_freebsd.go | 14 ++++++++++++++ wgtypes/errors.go | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 wgtypes/errors.go diff --git a/client_integration_test.go b/client_integration_test.go index c9bf0e0..def17b5 100644 --- a/client_integration_test.go +++ b/client_integration_test.go @@ -365,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 index fa55a3a..3ae49f9 100644 --- a/internal/wgfreebsd/client_freebsd.go +++ b/internal/wgfreebsd/client_freebsd.go @@ -166,6 +166,20 @@ func (c *Client) Device(name string) (*wgtypes.Device, error) { // 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 { 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") + From 86b20a7f1e384c926258079c774bc906b1c90f40 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Tue, 11 Oct 2022 21:38:01 +0200 Subject: [PATCH 6/7] test: increase test timeout for slow FreeBSD tests Signed-off-by: Steffen Vogel --- client_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client_integration_test.go b/client_integration_test.go index def17b5..ab4bedc 100644 --- a/client_integration_test.go +++ b/client_integration_test.go @@ -70,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() From aff2a3db2bcabf521b0451c92e3fec758d89d089 Mon Sep 17 00:00:00 2001 From: Steffen Vogel Date: Sun, 16 Oct 2022 15:26:47 +0200 Subject: [PATCH 7/7] add FreeBSD support to README Signed-off-by: Steffen Vogel --- README.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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.