netconfig: implement WireGuard support

To set up a tunnel, create a /perm/wireguard.json as illustrated in
netconfig_test.go, and don’t forget to adjust your /perm/interfaces.json with
the address configuration for the WireGuard tunnel.

Note that static routes cannot currently be configured, so the usefulness is
limited. If you have a use-case you’d like to see covered, please explain it in
https://github.com/rtr7/router7/issues/14
This commit is contained in:
Michael Stapelberg 2018-11-26 18:29:03 +01:00
parent b6a5227d49
commit ec4f1f4dc5
5 changed files with 475 additions and 4 deletions

View File

@ -15,6 +15,7 @@
package integration_test
import (
"bytes"
"fmt"
"io/ioutil"
"os"
@ -43,6 +44,52 @@ const goldenInterfaces = `
"spoof_hardware_addr": "02:73:53:00:b0:aa",
"name": "lan0",
"addr": "192.168.42.1/24"
},
{
"name": "wg0",
"addr": "fe80::1/64"
}
]
}
`
const goldenWireguard = `
{
"interfaces":[
{
"name": "wg0",
"private_key": "gBCV3afBKfW7RycmeZFMpJykvO+58KfSEIyavay90kE=",
"port": 51820,
"peers": [
{
"public_key": "ScxV5nQsUIaaOp3qdwPqRcgMkR3oR6nyi1tBLUovqBs=",
"endpoint": "192.168.42.23:12345",
"allowed_ips": [
"fe80::/64",
"10.0.137.0/24"
]
},
{
"public_key": "AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=",
"endpoint": "[::1]:12345",
"allowed_ips": [
"10.0.0.0/8"
]
}
]
},
{
"name": "wg1",
"private_key": "gBCV3afBKfW7RycmeZFMpJykvO+58KfSEIyavay90kE=",
"port": 51820,
"peers": [
{
"public_key": "ScxV5nQsUIaaOp3qdwPqRcgMkR3oR6nyi1tBLUovqBs=",
"allowed_ips": [
"fe80::/64"
]
}
]
}
]
}
@ -168,6 +215,7 @@ func TestNetconfig(t *testing.T) {
{"dhcp4/wire/lease.json", goldenDhcp4},
{"dhcp6/wire/lease.json", goldenDhcp6},
{"interfaces.json", goldenInterfaces},
{"wireguard.json", goldenWireguard},
{"portforwardings.json", pf},
} {
if err := os.MkdirAll(filepath.Join(tmp, filepath.Dir(golden.filename)), 0755); err != nil {
@ -283,6 +331,45 @@ func TestNetconfig(t *testing.T) {
}
})
t.Run("VerifyWireguard", func(t *testing.T) {
var stderr bytes.Buffer
wg := exec.Command("ip", "netns", "exec", ns, "wg", "show", "wg0")
wg.Stderr = &stderr
out, err := wg.Output()
if err != nil {
t.Fatalf("%v: %v (stderr: %v)", wg.Args, err, strings.TrimSpace(stderr.String()))
}
const want = `interface: wg0
public key: 3ck9nX4ylfXm0fq4pWJ9n8Jku4fvzIXBVe3BsCNldB8=
private key: (hidden)
listening port: 51820
peer: ScxV5nQsUIaaOp3qdwPqRcgMkR3oR6nyi1tBLUovqBs=
endpoint: 192.168.42.23:12345
allowed ips: 10.0.137.0/24, fe80::/64
peer: AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=
endpoint: [::1]:12345
allowed ips: 10.0.0.0/8`
if got := strings.TrimSpace(string(out)); got != want {
t.Fatalf("unexpected wg output: diff (-want +got):\n%s", diff.LineDiff(want, got))
}
out, err = exec.Command("ip", "-netns", ns, "address", "show", "dev", "wg0").Output()
if err != nil {
t.Fatal(err)
}
upRe := regexp.MustCompile(`wg0: <[^>]+,UP`)
if !upRe.MatchString(string(out)) {
t.Errorf("regexp %s does not match %s", upRe, string(out))
}
addr6Re := regexp.MustCompile(`(?m)^\s*inet6 fe80::1/64 scope link\s*$`)
if !addr6Re.MatchString(string(out)) {
t.Errorf("regexp %s does not match %s", addr6Re, string(out))
}
})
opts := []cmp.Option{
cmp.Transformer("formatting", func(line string) string {
return strings.TrimSpace(strings.Replace(line, "dnat to", "dnat", -1))

View File

@ -214,12 +214,14 @@ func applyInterfaces(dir, root string) error {
if err := json.Unmarshal(b, &cfg); err != nil {
return err
}
byName := make(map[string]InterfaceDetails)
byHardwareAddr := make(map[string]InterfaceDetails)
for _, details := range cfg.Interfaces {
byHardwareAddr[details.HardwareAddr] = details
if spoof := details.SpoofHardwareAddr; spoof != "" {
byHardwareAddr[spoof] = details
}
byName[details.Name] = details
}
links, err := netlink.LinkList()
for _, l := range links {
@ -227,13 +229,21 @@ func applyInterfaces(dir, root string) error {
// TODO: prefix log line with details about the interface.
// link &{LinkAttrs:{Index:2 MTU:1500 TxQLen:1000 Name:eth0 HardwareAddr:00:0d:b9:49:70:18 Flags:broadcast|multicast RawFlags:4098 ParentIndex:0 MasterIndex:0 Namespace:<nil> Alias: Statistics:0xc4200f45f8 Promisc:0 Xdp:0xc4200ca180 EncapType:ether Protinfo:<nil> OperState:down NetNsID:0 NumTxQueues:0 NumRxQueues:0 Vfs:[]}}, attr &{Index:2 MTU:1500 TxQLen:1000 Name:eth0 HardwareAddr:00:0d:b9:49:70:18 Flags:broadcast|multicast RawFlags:4098 ParentIndex:0 MasterIndex:0 Namespace:<nil> Alias: Statistics:0xc4200f45f8 Promisc:0 Xdp:0xc4200ca180 EncapType:ether Protinfo:<nil> OperState:down NetNsID:0 NumTxQueues:0 NumRxQueues:0 Vfs:[]}
var (
details InterfaceDetails
ok bool
)
addr := attr.HardwareAddr.String()
details, ok := byHardwareAddr[addr]
if !ok {
if addr == "" {
if addr == "" {
details, ok = byName[attr.Name]
if !ok {
continue // not a configurable interface (e.g. sit0)
}
log.Printf("no config for hardwareattr %s", addr)
} else {
details, ok = byHardwareAddr[addr]
}
if !ok {
log.Printf("no config for interface %s/%s", attr.Name, addr)
continue
}
log.Printf("apply details %+v", details)
@ -732,5 +742,9 @@ func Apply(dir, root string) error {
return fmt.Errorf("firewall: %v", err)
}
if err := applyWireGuard(dir); err != nil {
return fmt.Errorf("wireguard: %v", err)
}
return firstErr
}

View File

@ -0,0 +1,133 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package netconfig
import (
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"os"
"path/filepath"
"syscall"
"github.com/mdlayher/genetlink"
"github.com/rtr7/router7/internal/wg"
"github.com/vishvananda/netlink"
)
type wireguardPeer struct {
PublicKey string `json:"public_key"` // base64-encoded
Endpoint string `json:"endpoint"` // e.g. “[::1]:12345”
AllowedIPs []string `json:"allowed_ips"` // e.g. “["fe80::/64", "10.0.137.0/24"]”
}
type wireguardInterface struct {
Name string `json:"name"` // e.g. “wg0”
PrivateKey string `json:"private_key"` // base64-encoded
Port int `json:"port"` // e.g. “51820”
Peers []wireguardPeer `json:"peers"`
}
type wireguardInterfaces struct {
Interfaces []wireguardInterface `json:"interfaces"`
}
type wgLink struct {
name string
}
func (w *wgLink) Type() string {
return "wireguard"
}
func (w *wgLink) Attrs() *netlink.LinkAttrs {
attrs := netlink.NewLinkAttrs()
attrs.Name = w.name
return &attrs
}
func applyWireGuard(dir string) error {
b, err := ioutil.ReadFile(filepath.Join(dir, "wireguard.json"))
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
var cfg wireguardInterfaces
if err := json.Unmarshal(b, &cfg); err != nil {
return err
}
h, err := netlink.NewHandle()
if err != nil {
return fmt.Errorf("netlink.NewHandle: %v", err)
}
defer h.Delete()
conn, err := genetlink.Dial(nil)
if err != nil {
return fmt.Errorf("genetlink.Dial: %v", err)
}
defer conn.Close()
for _, iface := range cfg.Interfaces {
l := &wgLink{iface.Name}
if err := h.LinkAdd(l); err != nil {
if ee, ok := err.(syscall.Errno); !ok || ee != syscall.EEXIST {
return fmt.Errorf("LinkAdd(%v): %v", l, err)
}
}
var peers []*wg.Peer
for _, p := range iface.Peers {
var ips []*net.IPNet
for _, ip := range p.AllowedIPs {
_, ipnet, err := net.ParseCIDR(ip)
if err != nil {
return err
}
ips = append(ips, ipnet)
}
b, err := base64.StdEncoding.DecodeString(p.PublicKey)
if err != nil {
return err
}
peers = append(peers, &wg.Peer{
PublicKey: b,
Endpoint: p.Endpoint,
AllowedIPs: ips,
})
}
b, err := base64.StdEncoding.DecodeString(iface.PrivateKey)
if err != nil {
return err
}
d := &wg.Device{
Ifname: iface.Name,
PrivateKey: b,
ListenPort: uint16(iface.Port),
Peers: peers,
}
if err := wg.SetDevice(conn, d); err != nil {
return err
}
}
return nil
}

155
internal/wg/setdevice.go Normal file
View File

@ -0,0 +1,155 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wg
import (
"fmt"
"net"
"unsafe"
"github.com/google/nftables/binaryutil"
"github.com/mdlayher/genetlink"
"github.com/mdlayher/netlink"
"golang.org/x/sys/unix"
)
func allowedIPFromNet(n *net.IPNet) ([]byte, error) {
ones, _ := n.Mask.Size()
family := uint16(unix.AF_INET)
if n.IP.To4() == nil {
family = unix.AF_INET6
}
return netlink.MarshalAttributes([]netlink.Attribute{
{Type: wgallowedip_a_family, Data: binaryutil.NativeEndian.PutUint16(family)},
{Type: wgallowedip_a_ipaddr, Data: n.IP},
{Type: wgallowedip_a_cidr_mask, Data: []byte{byte(ones)}},
})
}
func sockaddrFromEndpoint(endpoint string) ([]byte, error) {
host, service, err := net.SplitHostPort(endpoint)
if err != nil {
return nil, err
}
ip := net.ParseIP(host)
if ip == nil {
return nil, fmt.Errorf("invalid endpoint %q: %q is not an IP", endpoint, host)
}
port, err := net.LookupPort("udp4", service)
if err != nil {
return nil, err
}
if ip.To4() == nil {
addr := unix.RawSockaddrInet6{
Family: unix.AF_INET6,
Port: uint16((port&0xFF)<<8) | uint16((port&0xFF00)>>8),
Addr: func() [16]byte {
var buf [16]byte
copy(buf[:], ip)
return buf
}(),
}
sap := (*[28]byte)(unsafe.Pointer(&addr))
return (*sap)[:], nil
} else {
addr := unix.RawSockaddrInet4{
Family: unix.AF_INET,
Port: uint16((port&0xFF)<<8) | uint16((port&0xFF00)>>8),
Addr: func() [4]byte {
var buf [4]byte
copy(buf[:], ip.To4())
return buf
}(),
}
sap := (*[16]byte)(unsafe.Pointer(&addr))
return (*sap)[:], nil
}
}
func SetDevice(conn *genetlink.Conn, d *Device) error {
family, err := conn.GetFamily("wireguard")
if err != nil {
return err
}
var peers []netlink.Attribute
for _, p := range d.Peers {
var ips []netlink.Attribute
for _, net := range p.AllowedIPs {
allowedIP, err := allowedIPFromNet(net)
if err != nil {
return err
}
ips = append(ips, netlink.Attribute{Type: unix.NLA_F_NESTED, Data: allowedIP})
}
allowedIPs, err := netlink.MarshalAttributes(ips)
if err != nil {
return err
}
attrs := []netlink.Attribute{
{Type: wgpeer_a_public_key, Data: p.PublicKey},
{Type: wgpeer_a_flags, Data: binaryutil.NativeEndian.PutUint32(0)},
{Type: wgpeer_a_persistent_keepalive_interval, Data: binaryutil.NativeEndian.PutUint16(0)},
{Type: wgpeer_a_allowedips, Data: allowedIPs},
}
if p.Endpoint != "" {
sockaddr, err := sockaddrFromEndpoint(p.Endpoint)
if err != nil {
return err
}
attrs = append(attrs, netlink.Attribute{Type: wgpeer_a_endpoint, Data: sockaddr})
}
peer, err := netlink.MarshalAttributes(attrs)
if err != nil {
return err
}
peers = append(peers, netlink.Attribute{Type: unix.NLA_F_NESTED, Data: peer})
}
peersData, err := netlink.MarshalAttributes(peers)
if err != nil {
return err
}
data, err := netlink.MarshalAttributes([]netlink.Attribute{
{Type: wgdevice_a_ifname, Data: []byte(d.Ifname + "\x00")},
{Type: wgdevice_a_flags, Data: binaryutil.NativeEndian.PutUint32(0)},
{Type: wgdevice_a_private_key, Data: d.PrivateKey},
{Type: wgdevice_a_listen_port, Data: binaryutil.NativeEndian.PutUint16(d.ListenPort)},
{Type: wgdevice_a_fwmark, Data: binaryutil.NativeEndian.PutUint32(0)},
{Type: unix.NLA_F_NESTED | wgdevice_a_peers, Data: peersData},
})
if err != nil {
return err
}
get := genetlink.Message{
Header: genetlink.Header{
Command: wg_cmd_set_device,
Version: family.Version,
},
Data: data,
}
const flags = netlink.HeaderFlagsRequest | netlink.HeaderFlagsAcknowledge
reply, err := conn.Execute(get, family.ID, flags)
if err != nil {
return err
}
if got, want := len(reply), 1; got != want {
return fmt.Errorf("unexpected number of replies: got %d, want %d", got, want)
}
return nil
}

82
internal/wg/types.go Normal file
View File

@ -0,0 +1,82 @@
// Copyright 2018 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package wg implements the WireGuard Linux kernel modules generic netlink
// interface, e.g. for configuring WireGuard network interfaces (e.g. wg0)
// without resorting to the wg command line tool.
package wg
import (
"net"
"time"
)
const (
wg_cmd_get_device = iota
wg_cmd_set_device
)
const (
wgdevice_a_unspec = iota
wgdevice_a_ifindex
wgdevice_a_ifname
wgdevice_a_private_key
wgdevice_a_public_key
wgdevice_a_flags
wgdevice_a_listen_port
wgdevice_a_fwmark
wgdevice_a_peers
)
const (
wgpeer_a_unspec = iota
wgpeer_a_public_key
wgpeer_a_preshared_key
wgpeer_a_flags
wgpeer_a_endpoint
wgpeer_a_persistent_keepalive_interval
wgpeer_a_last_handshake_time
wgpeer_a_rx_bytes
wgpeer_a_tx_bytes
wgpeer_a_allowedips
wgpeer_a_protocol_version
)
const (
wgallowedip_a_unspec = iota
wgallowedip_a_family
wgallowedip_a_ipaddr
wgallowedip_a_cidr_mask
)
type Peer struct {
PublicKey []byte
PresharedKey []byte
PersistentKeepaliveInterval uint16
LastHandshakeTime time.Time
RxBytes uint64
TxBytes uint64
AllowedIPs []*net.IPNet
ProtocolVersion uint32
Endpoint string
}
type Device struct {
Ifindex uint32
Ifname string
PrivateKey []byte
PublicKey []byte
ListenPort uint16
Fwmark uint32
Peers []*Peer
}