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:
parent
b6a5227d49
commit
ec4f1f4dc5
@ -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))
|
||||
|
@ -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
|
||||
}
|
||||
|
133
internal/netconfig/wireguard.go
Normal file
133
internal/netconfig/wireguard.go
Normal 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
155
internal/wg/setdevice.go
Normal 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
82
internal/wg/types.go
Normal 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 module’s 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
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user