wireguard: wg show iface dump reader and parser

This commit is contained in:
Julien Viard de Galbert 2021-04-21 16:32:03 +02:00
parent 0733c83a0a
commit 98ee23aced
3 changed files with 209 additions and 41 deletions

View File

@ -17,11 +17,13 @@ package wireguard
import ( import (
"bufio" "bufio"
"bytes" "bytes"
"errors"
"fmt" "fmt"
"net" "net"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
) )
@ -31,6 +33,9 @@ type key string
const ( const (
separator = "=" separator = "="
dumpSeparator = "\t"
dumpNone = "(none)"
dumpOff = "off"
interfaceSection section = "Interface" interfaceSection section = "Interface"
peerSection section = "Peer" peerSection section = "Peer"
listenPortKey key = "ListenPort" listenPortKey key = "ListenPort"
@ -61,6 +66,8 @@ type Peer struct {
PersistentKeepalive int PersistentKeepalive int
PresharedKey []byte PresharedKey []byte
PublicKey []byte PublicKey []byte
// The following fields are part of the runtime information, not the configuration.
LatestHandshake time.Time
} }
// DeduplicateIPs eliminates duplicate allowed IPs. // DeduplicateIPs eliminates duplicate allowed IPs.
@ -146,13 +153,11 @@ func (d DNSOrIP) String() string {
func Parse(buf []byte) *Conf { func Parse(buf []byte) *Conf {
var ( var (
active section active section
ai *net.IPNet
kv []string kv []string
c Conf c Conf
err error err error
iface *Interface iface *Interface
i int i int
ip, ip4 net.IP
k key k key
line, v string line, v string
peer *Peer peer *Peer
@ -205,49 +210,15 @@ func Parse(buf []byte) *Conf {
case peerSection: case peerSection:
switch k { switch k {
case allowedIPsKey: case allowedIPsKey:
// Reuse string slice. err = peer.parseAllowedIPs(v)
kv = strings.Split(v, ",")
for i = range kv {
ip, ai, err = net.ParseCIDR(strings.TrimSpace(kv[i]))
if err != nil { if err != nil {
continue continue
} }
if ip4 = ip.To4(); ip4 != nil {
ip = ip4
} else {
ip = ip.To16()
}
ai.IP = ip
peer.AllowedIPs = append(peer.AllowedIPs, ai)
}
case endpointKey: case endpointKey:
// Reuse string slice. err = peer.parseEndpoint(v)
kv = strings.Split(v, ":")
if len(kv) < 2 {
continue
}
port, err = strconv.ParseUint(kv[len(kv)-1], 10, 32)
if err != nil { if err != nil {
continue continue
} }
d := DNSOrIP{}
ip = net.ParseIP(strings.Trim(strings.Join(kv[:len(kv)-1], ":"), "[]"))
if ip == nil {
if len(validation.IsDNS1123Subdomain(kv[0])) != 0 {
continue
}
d.DNS = kv[0]
} else {
if ip4 = ip.To4(); ip4 != nil {
d.IP = ip4
} else {
d.IP = ip.To16()
}
}
peer.Endpoint = &Endpoint{
DNSOrIP: d,
Port: uint32(port),
}
case persistentKeepaliveKey: case persistentKeepaliveKey:
i, err = strconv.Atoi(v) i, err = strconv.Atoi(v)
if err != nil { if err != nil {
@ -448,3 +419,142 @@ func writeKey(buf *bytes.Buffer, k key) error {
_, err = buf.WriteString(" = ") _, err = buf.WriteString(" = ")
return err return err
} }
var (
errParseEndpoint = errors.New("could not parse Endpoint")
)
func (p *Peer) parseEndpoint(v string) error {
var (
kv []string
err error
ip, ip4 net.IP
port uint64
)
kv = strings.Split(v, ":")
if len(kv) < 2 {
return errParseEndpoint
}
port, err = strconv.ParseUint(kv[len(kv)-1], 10, 32)
if err != nil {
return err
}
d := DNSOrIP{}
ip = net.ParseIP(strings.Trim(strings.Join(kv[:len(kv)-1], ":"), "[]"))
if ip == nil {
if len(validation.IsDNS1123Subdomain(kv[0])) != 0 {
return errParseEndpoint
}
d.DNS = kv[0]
} else {
if ip4 = ip.To4(); ip4 != nil {
d.IP = ip4
} else {
d.IP = ip.To16()
}
}
p.Endpoint = &Endpoint{
DNSOrIP: d,
Port: uint32(port),
}
return nil
}
func (p *Peer) parseAllowedIPs(v string) error {
var (
ai *net.IPNet
kv []string
err error
i int
ip, ip4 net.IP
)
kv = strings.Split(v, ",")
for i = range kv {
ip, ai, err = net.ParseCIDR(strings.TrimSpace(kv[i]))
if err != nil {
return err
}
if ip4 = ip.To4(); ip4 != nil {
ip = ip4
} else {
ip = ip.To16()
}
ai.IP = ip
p.AllowedIPs = append(p.AllowedIPs, ai)
}
return nil
}
// ParseDump parses a given WireGuard dump and produces a Conf struct.
func ParseDump(buf []byte) *Conf {
// from man wg, show section:
// If dump is specified, then several lines are printed;
// the first contains in order separated by tab: private-key, public-key, listen-port, fwmark.
// Subsequent lines are printed for each peer and contain in order separated by tab:
// public-key, preshared-key, endpoint, allowed-ips, latest-handshake, transfer-rx, transfer-tx, persistent-keepalive.
var (
active section
values []string
c Conf
err error
iface *Interface
i int
peer *Peer
port uint64
sec int64
)
// First line is Interface
active = interfaceSection
s := bufio.NewScanner(bytes.NewBuffer(buf))
for s.Scan() {
values = strings.Split(s.Text(), dumpSeparator)
switch active {
case interfaceSection:
if len(values) < 4 {
break
}
iface = new(Interface)
iface.PrivateKey = []byte(values[0])
port, _ = strconv.ParseUint(values[2], 10, 32)
iface.ListenPort = uint32(port)
c.Interface = iface
// Next lines are Peers
active = peerSection
case peerSection:
if len(values) < 8 {
break
}
peer = new(Peer)
peer.PublicKey = []byte(values[0])
if values[1] != dumpNone {
peer.PresharedKey = []byte(values[1])
}
if values[2] != dumpNone {
peer.parseEndpoint(values[2])
}
if values[3] != dumpNone {
peer.parseAllowedIPs(values[3])
}
if values[4] != "0" {
sec, err = strconv.ParseInt(values[4], 10, 64)
if err == nil {
peer.LatestHandshake = time.Unix(sec, 0)
}
}
if values[7] != dumpOff {
i, _ = strconv.Atoi(values[7])
peer.PersistentKeepalive = i
}
c.Peers = append(c.Peers, peer)
peer = nil
}
}
return &c
}

View File

@ -17,6 +17,8 @@ package wireguard
import ( import (
"net" "net"
"testing" "testing"
"github.com/kylelemons/godebug/pretty"
) )
func TestCompareConf(t *testing.T) { func TestCompareConf(t *testing.T) {
@ -308,3 +310,47 @@ func TestCompareEndpoint(t *testing.T) {
} }
} }
} }
func TestCompareDumpConf(t *testing.T) {
for _, tc := range []struct {
name string
d []byte
c []byte
}{
{
name: "empty",
d: []byte{},
c: []byte{},
},
{
name: "redacted copy from wg output",
d: []byte(`private B7qk8EMlob0nfado0ABM6HulUV607r4yqtBKjhap7S4= 51820 off
key1 (none) 10.254.1.1:51820 100.64.1.0/24,192.168.0.125/32,10.4.0.1/32 1619012801 67048 34952 10
key2 (none) 10.254.2.1:51820 100.64.4.0/24,10.69.76.55/32,100.64.3.0/24,10.66.25.131/32,10.4.0.2/32 1619013058 1134456 10077852 10`),
c: []byte(`[Interface]
ListenPort = 51820
PrivateKey = private
[Peer]
PublicKey = key1
AllowedIPs = 100.64.1.0/24, 192.168.0.125/32, 10.4.0.1/32
Endpoint = 10.254.1.1:51820
PersistentKeepalive = 10
[Peer]
PublicKey = key2
AllowedIPs = 100.64.4.0/24, 10.69.76.55/32, 100.64.3.0/24, 10.66.25.131/32, 10.4.0.2/32
Endpoint = 10.254.2.1:51820
PersistentKeepalive = 10`),
},
} {
dumpConf := ParseDump(tc.d)
conf := Parse(tc.c)
// Equal will ignore runtime fields and only compare configuration fields.
if !dumpConf.Equal(conf) {
diff := pretty.Compare(dumpConf, conf)
t.Errorf("test case %q: got diff: %v", tc.name, diff)
}
}
}

View File

@ -119,3 +119,15 @@ func ShowConf(iface string) ([]byte, error) {
} }
return stdout.Bytes(), nil return stdout.Bytes(), nil
} }
// ShowDump gets the WireGuard configuration and runtime information for the given interface.
func ShowDump(iface string) ([]byte, error) {
cmd := exec.Command("wg", "show", iface, "dump")
var stderr, stdout bytes.Buffer
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
return nil, fmt.Errorf("failed to read the WireGuard dump output: %s", stderr.String())
}
return stdout.Bytes(), nil
}