// Package wginterface demonstrates creating and destroying a WireGuard network // interface using only raw system calls — no netlink library. // // Creating a typed interface (kind = "wireguard") requires the NETLINK_ROUTE // protocol; there is no ioctl path for it. Everything else — assigning an IP // address and bringing the link up — can be done with the older AF_INET ioctl // interface, exactly as one would for a TUN device. // // The package requires CAP_NET_ADMIN and the wireguard kernel module. package wginterface import ( "encoding/binary" "fmt" "net" "slices" "golang.org/x/sys/unix" ) // Create creates a WireGuard interface named name, assigns vpnIP/prefixLen to // it, and brings it up. func Create(name string, vpnIP net.IP, prefixLen int) error { _ = Delete(name) // remove any stale interface left by a previous run if err := nlNewLink(name); err != nil { return fmt.Errorf("failed to create wireguard link: %w", err) } if err := ioctlSetAddr(name, vpnIP, prefixLen); err != nil { _ = Delete(name) return fmt.Errorf("assign address: %w", err) } if err := ioctlLinkUp(name); err != nil { _ = Delete(name) return fmt.Errorf("link up: %w", err) } return nil } // Delete removes the named interface. func Delete(name string) error { return nlDelLink(name) } // --------------------------------------------------------------------------- // Netlink link management // // Creating a WireGuard interface requires an RTM_NEWLINK message with a nested // IFLA_LINKINFO attribute whose IFLA_INFO_KIND is "wireguard". The full // message layout is: // // nlmsghdr (16 bytes) // ifinfomsg (16 bytes, all zeros for a new link) // rtattr IFLA_IFNAME → name + \0 // rtattr IFLA_LINKINFO // rtattr IFLA_INFO_KIND → "wireguard" + \0 // // All multi-byte integers are in native byte order (little-endian on // x86/arm64). Every attribute is padded to a 4-byte boundary; the len field // in the header records the unpadded length but the attribute occupies the // padded size. const ( nlmsgHdrLen = 16 // sizeof(struct nlmsghdr) sizeofIfInfo = 16 // sizeof(struct ifinfomsg) // Attribute types not exposed by the unix package at the level we need. iflaLinkInfo = 18 // IFLA_LINKINFO — container for link-type attributes iflaInfoKind = 1 // IFLA_INFO_KIND — link type string, nested inside IFLA_LINKINFO ) // nlNewLink creates the wireguard interface using Netlink. func nlNewLink(name string) error { // Build innermost attribute first, then wrap outward. infoKind := nlAttr(iflaInfoKind, cstring("wireguard")) linkInfo := nlAttr(iflaLinkInfo, infoKind) ifName := nlAttr(unix.IFLA_IFNAME, cstring(name)) // ifinfomsg: all-zero = AF_UNSPEC, no index, no flags (kernel assigns index). ifInfo := make([]byte, sizeofIfInfo) payload := slices.Concat(ifInfo, ifName, linkInfo) flags := uint16(unix.NLM_F_REQUEST | unix.NLM_F_ACK | unix.NLM_F_CREATE | unix.NLM_F_EXCL) return nlRoundtrip(unix.RTM_NEWLINK, flags, payload) } func nlDelLink(name string) error { iface, err := net.InterfaceByName(name) if err != nil { return err } // For RTM_DELLINK the kernel identifies the link by ifi_index. ifi_index // sits at byte offset 4 in the ifinfomsg struct. ifInfo := make([]byte, sizeofIfInfo) binary.NativeEndian.PutUint32(ifInfo[4:8], uint32(iface.Index)) return nlRoundtrip(unix.RTM_DELLINK, uint16(unix.NLM_F_REQUEST|unix.NLM_F_ACK), ifInfo) } // nlRoundtrip opens a NETLINK_ROUTE socket, sends one request, reads the // NLMSG_ERROR acknowledgement, and closes the socket. func nlRoundtrip(msgType uint16, flags uint16, payload []byte) error { fd, err := unix.Socket(unix.AF_NETLINK, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.NETLINK_ROUTE) if err != nil { return fmt.Errorf("socket: %w", err) } defer unix.Close(fd) if err := unix.Bind(fd, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { return fmt.Errorf("bind: %w", err) } msg := nlMsg(msgType, flags, payload) if err := unix.Sendto(fd, msg, 0, &unix.SockaddrNetlink{Family: unix.AF_NETLINK}); err != nil { return fmt.Errorf("sendto: %w", err) } resp := make([]byte, 4096) n, _, err := unix.Recvfrom(fd, resp, 0) if err != nil { return fmt.Errorf("recvfrom: %w", err) } return nlAckErr(resp[:n]) } // nlMsg prepends an nlmsghdr to payload. func nlMsg(msgType uint16, flags uint16, payload []byte) []byte { buf := make([]byte, nlmsgHdrLen+len(payload)) binary.NativeEndian.PutUint32(buf[0:4], uint32(len(buf))) // nlmsg_len binary.NativeEndian.PutUint16(buf[4:6], msgType) // nlmsg_type binary.NativeEndian.PutUint16(buf[6:8], flags) // nlmsg_flags binary.NativeEndian.PutUint32(buf[8:12], 1) // nlmsg_seq binary.NativeEndian.PutUint32(buf[12:16], 0) // nlmsg_pid (0 = kernel) copy(buf[nlmsgHdrLen:], payload) return buf } // nlAckErr parses an NLMSG_ERROR response. The error field is a negated errno // (0 = success, -EEXIST = interface exists, etc.). func nlAckErr(resp []byte) error { if len(resp) < nlmsgHdrLen+4 { return fmt.Errorf("netlink response too short (%d bytes)", len(resp)) } if binary.NativeEndian.Uint16(resp[4:6]) != unix.NLMSG_ERROR { return fmt.Errorf("unexpected nlmsg_type %d", binary.NativeEndian.Uint16(resp[4:6])) } // Error code follows the nlmsghdr; it is a signed int32 holding -errno. code := int32(binary.NativeEndian.Uint32(resp[nlmsgHdrLen:])) if code != 0 { return unix.Errno(-code) } return nil } // nlAttr encodes one netlink attribute: [len:u16][type:u16][data][pad to 4 // bytes]. The len field counts the header + data (before padding); the // allocation is padded so that the next attribute starts on a 4-byte boundary. func nlAttr(attrType uint16, data []byte) []byte { const hdr = 4 attrLen := hdr + len(data) padded := (attrLen + 3) &^ 3 buf := make([]byte, padded) binary.NativeEndian.PutUint16(buf[0:2], uint16(attrLen)) binary.NativeEndian.PutUint16(buf[2:4], attrType) copy(buf[hdr:], data) return buf } // --------------------------------------------------------------------------- // ioctl-based address assignment and link-up // // These operations could also be done via RTM_NEWADDR / RTM_NEWLINK netlink // messages, but the AF_INET ioctl interface is simpler. func ioctlSetAddr(name string, ip net.IP, prefixLen int) error { fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP) if err != nil { return err } defer unix.Close(fd) req, err := unix.NewIfreq(name) if err != nil { return err } if err := req.SetInet4Addr(ip.To4()); err != nil { return err } if err := unix.IoctlIfreq(fd, unix.SIOCSIFADDR, req); err != nil { return err } req, err = unix.NewIfreq(name) if err != nil { return err } mask := net.CIDRMask(prefixLen, 32) if err := req.SetInet4Addr([]byte(mask)); err != nil { return err } return unix.IoctlIfreq(fd, unix.SIOCSIFNETMASK, req) } func ioctlLinkUp(name string) error { fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, unix.IPPROTO_IP) if err != nil { return err } defer unix.Close(fd) req, err := unix.NewIfreq(name) if err != nil { return err } if err := unix.IoctlIfreq(fd, unix.SIOCGIFFLAGS, req); err != nil { return err } req.SetUint16(req.Uint16() | unix.IFF_UP | unix.IFF_RUNNING) return unix.IoctlIfreq(fd, unix.SIOCSIFFLAGS, req) } // cstring returns b as a null-terminated byte slice. func cstring(s string) []byte { return append([]byte(s), 0) }