Refactor - now wireguard based. (#7)

This commit is contained in:
2026-06-12 15:11:01 +00:00
parent 5ae075647d
commit 9a3cb2d1c2
105 changed files with 3776 additions and 4251 deletions

View File

@@ -0,0 +1,225 @@
// 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)
}