264 lines
6.5 KiB
Go
264 lines
6.5 KiB
Go
//go:build linux
|
|
// +build linux
|
|
|
|
package wglinux
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"syscall"
|
|
|
|
"github.com/mdlayher/genetlink"
|
|
"github.com/mdlayher/netlink"
|
|
"github.com/mdlayher/netlink/nlenc"
|
|
"golang.org/x/sys/unix"
|
|
"golang.zx2c4.com/wireguard/wgctrl/internal/wginternal"
|
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
|
)
|
|
|
|
var _ wginternal.Client = &Client{}
|
|
|
|
// A Client provides access to Linux WireGuard netlink information.
|
|
type Client struct {
|
|
c *genetlink.Conn
|
|
family genetlink.Family
|
|
|
|
interfaces func() ([]string, error)
|
|
}
|
|
|
|
// New creates a new Client and returns whether or not the generic netlink
|
|
// interface is available.
|
|
func New() (*Client, bool, error) {
|
|
c, err := genetlink.Dial(&netlink.Config{Strict: true})
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
|
|
return initClient(c)
|
|
}
|
|
|
|
// initClient is the internal Client constructor used in some tests.
|
|
func initClient(c *genetlink.Conn) (*Client, bool, error) {
|
|
f, err := c.GetFamily(unix.WG_GENL_NAME)
|
|
if err != nil {
|
|
_ = c.Close()
|
|
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
// The generic netlink interface is not available.
|
|
return nil, false, nil
|
|
}
|
|
|
|
return nil, false, err
|
|
}
|
|
|
|
return &Client{
|
|
c: c,
|
|
family: f,
|
|
|
|
// By default, gather only WireGuard interfaces using rtnetlink.
|
|
interfaces: rtnlInterfaces,
|
|
}, true, nil
|
|
}
|
|
|
|
// Close implements wginternal.Client.
|
|
func (c *Client) Close() error {
|
|
return c.c.Close()
|
|
}
|
|
|
|
// Devices implements wginternal.Client.
|
|
func (c *Client) Devices() ([]*wgtypes.Device, error) {
|
|
// By default, rtnetlink is used to fetch a list of all interfaces and then
|
|
// filter that list to only find WireGuard interfaces.
|
|
//
|
|
// The remainder of this function assumes that any returned device from this
|
|
// function is a valid WireGuard device.
|
|
ifis, err := c.interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ds := make([]*wgtypes.Device, 0, len(ifis))
|
|
for _, ifi := range ifis {
|
|
d, err := c.Device(ifi)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ds = append(ds, d)
|
|
}
|
|
|
|
return ds, nil
|
|
}
|
|
|
|
// Device implements wginternal.Client.
|
|
func (c *Client) Device(name string) (*wgtypes.Device, error) {
|
|
// Don't bother querying netlink with empty input.
|
|
if name == "" {
|
|
return nil, os.ErrNotExist
|
|
}
|
|
|
|
// Fetching a device by interface index is possible as well, but we only
|
|
// support fetching by name as it seems to be more convenient in general.
|
|
b, err := netlink.MarshalAttributes([]netlink.Attribute{{
|
|
Type: unix.WGDEVICE_A_IFNAME,
|
|
Data: nlenc.Bytes(name),
|
|
}})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
msgs, err := c.execute(unix.WG_CMD_GET_DEVICE, netlink.Request|netlink.Dump, b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return parseDevice(msgs)
|
|
}
|
|
|
|
// ConfigureDevice implements wginternal.Client.
|
|
func (c *Client) ConfigureDevice(name string, cfg wgtypes.Config) error {
|
|
// Large configurations are split into batches for use with netlink.
|
|
for _, b := range buildBatches(cfg) {
|
|
attrs, err := configAttrs(name, b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Request acknowledgement of our request from netlink, even though the
|
|
// output messages are unused. The netlink package checks and trims the
|
|
// status code value.
|
|
if _, err := c.execute(unix.WG_CMD_SET_DEVICE, netlink.Request|netlink.Acknowledge, attrs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// execute executes a single WireGuard netlink request with the specified command,
|
|
// header flags, and attribute arguments.
|
|
func (c *Client) execute(command uint8, flags netlink.HeaderFlags, attrb []byte) ([]genetlink.Message, error) {
|
|
msg := genetlink.Message{
|
|
Header: genetlink.Header{
|
|
Command: command,
|
|
Version: unix.WG_GENL_VERSION,
|
|
},
|
|
Data: attrb,
|
|
}
|
|
|
|
msgs, err := c.c.Execute(msg, c.family.ID, flags)
|
|
if err == nil {
|
|
return msgs, nil
|
|
}
|
|
|
|
// We don't want to expose netlink errors directly to callers so unpack to
|
|
// something more generic.
|
|
oerr, ok := err.(*netlink.OpError)
|
|
if !ok {
|
|
// Expect all errors to conform to netlink.OpError.
|
|
return nil, fmt.Errorf("wglinux: netlink operation returned non-netlink error (please file a bug: https://golang.zx2c4.com/wireguard/wgctrl): %v", err)
|
|
}
|
|
|
|
switch oerr.Err {
|
|
// Convert "no such device" and "not a wireguard device" to an error
|
|
// compatible with os.ErrNotExist for easy checking.
|
|
case unix.ENODEV, unix.ENOTSUP:
|
|
return nil, os.ErrNotExist
|
|
default:
|
|
// Expose the inner error directly (such as EPERM).
|
|
return nil, oerr.Err
|
|
}
|
|
}
|
|
|
|
// rtnlInterfaces uses rtnetlink to fetch a list of WireGuard interfaces.
|
|
func rtnlInterfaces() ([]string, error) {
|
|
// Use the stdlib's rtnetlink helpers to get ahold of a table of all
|
|
// interfaces, so we can begin filtering it down to just WireGuard devices.
|
|
tab, err := syscall.NetlinkRIB(unix.RTM_GETLINK, unix.AF_UNSPEC)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wglinux: failed to get list of interfaces from rtnetlink: %v", err)
|
|
}
|
|
|
|
msgs, err := syscall.ParseNetlinkMessage(tab)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("wglinux: failed to parse rtnetlink messages: %v", err)
|
|
}
|
|
|
|
return parseRTNLInterfaces(msgs)
|
|
}
|
|
|
|
// parseRTNLInterfaces unpacks rtnetlink messages and returns WireGuard
|
|
// interface names.
|
|
func parseRTNLInterfaces(msgs []syscall.NetlinkMessage) ([]string, error) {
|
|
var ifis []string
|
|
for _, m := range msgs {
|
|
// Only deal with link messages, and they must have an ifinfomsg
|
|
// structure appear before the attributes.
|
|
if m.Header.Type != unix.RTM_NEWLINK {
|
|
continue
|
|
}
|
|
|
|
if len(m.Data) < unix.SizeofIfInfomsg {
|
|
return nil, fmt.Errorf("wglinux: rtnetlink message is too short for ifinfomsg: %d", len(m.Data))
|
|
}
|
|
|
|
ad, err := netlink.NewAttributeDecoder(m.Data[syscall.SizeofIfInfomsg:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Determine the interface's name and if it's a WireGuard device.
|
|
var (
|
|
ifi string
|
|
isWG bool
|
|
)
|
|
|
|
for ad.Next() {
|
|
switch ad.Type() {
|
|
case unix.IFLA_IFNAME:
|
|
ifi = ad.String()
|
|
case unix.IFLA_LINKINFO:
|
|
ad.Do(isWGKind(&isWG))
|
|
}
|
|
}
|
|
|
|
if err := ad.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if isWG {
|
|
// Found one; append it to the list.
|
|
ifis = append(ifis, ifi)
|
|
}
|
|
}
|
|
|
|
return ifis, nil
|
|
}
|
|
|
|
// wgKind is the IFLA_INFO_KIND value for WireGuard devices.
|
|
const wgKind = "wireguard"
|
|
|
|
// isWGKind parses netlink attributes to determine if a link is a WireGuard
|
|
// device, then populates ok with the result.
|
|
func isWGKind(ok *bool) func(b []byte) error {
|
|
return func(b []byte) error {
|
|
ad, err := netlink.NewAttributeDecoder(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for ad.Next() {
|
|
if ad.Type() != unix.IFLA_INFO_KIND {
|
|
continue
|
|
}
|
|
|
|
if ad.String() == wgKind {
|
|
*ok = true
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return ad.Err()
|
|
}
|
|
}
|