From 98ee23aced44d9ac363132a7e1e6acf8b7962dac Mon Sep 17 00:00:00 2001 From: Julien Viard de Galbert Date: Wed, 21 Apr 2021 16:32:03 +0200 Subject: [PATCH] wireguard: `wg show iface dump` reader and parser --- pkg/wireguard/conf.go | 192 +++++++++++++++++++++++++++++-------- pkg/wireguard/conf_test.go | 46 +++++++++ pkg/wireguard/wireguard.go | 12 +++ 3 files changed, 209 insertions(+), 41 deletions(-) diff --git a/pkg/wireguard/conf.go b/pkg/wireguard/conf.go index 0ce55e3..92d50c8 100644 --- a/pkg/wireguard/conf.go +++ b/pkg/wireguard/conf.go @@ -17,11 +17,13 @@ package wireguard import ( "bufio" "bytes" + "errors" "fmt" "net" "sort" "strconv" "strings" + "time" "k8s.io/apimachinery/pkg/util/validation" ) @@ -31,6 +33,9 @@ type key string const ( separator = "=" + dumpSeparator = "\t" + dumpNone = "(none)" + dumpOff = "off" interfaceSection section = "Interface" peerSection section = "Peer" listenPortKey key = "ListenPort" @@ -61,6 +66,8 @@ type Peer struct { PersistentKeepalive int PresharedKey []byte PublicKey []byte + // The following fields are part of the runtime information, not the configuration. + LatestHandshake time.Time } // DeduplicateIPs eliminates duplicate allowed IPs. @@ -146,13 +153,11 @@ func (d DNSOrIP) String() string { func Parse(buf []byte) *Conf { var ( active section - ai *net.IPNet kv []string c Conf err error iface *Interface i int - ip, ip4 net.IP k key line, v string peer *Peer @@ -205,48 +210,14 @@ func Parse(buf []byte) *Conf { case peerSection: switch k { case allowedIPsKey: - // Reuse string slice. - kv = strings.Split(v, ",") - for i = range kv { - ip, ai, err = net.ParseCIDR(strings.TrimSpace(kv[i])) - if err != nil { - continue - } - if ip4 = ip.To4(); ip4 != nil { - ip = ip4 - } else { - ip = ip.To16() - } - ai.IP = ip - peer.AllowedIPs = append(peer.AllowedIPs, ai) - } - case endpointKey: - // Reuse string slice. - kv = strings.Split(v, ":") - if len(kv) < 2 { - continue - } - port, err = strconv.ParseUint(kv[len(kv)-1], 10, 32) + err = peer.parseAllowedIPs(v) if err != nil { 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 endpointKey: + err = peer.parseEndpoint(v) + if err != nil { + continue } case persistentKeepaliveKey: i, err = strconv.Atoi(v) @@ -448,3 +419,142 @@ func writeKey(buf *bytes.Buffer, k key) error { _, err = buf.WriteString(" = ") 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, fw‐mark. + // 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 +} diff --git a/pkg/wireguard/conf_test.go b/pkg/wireguard/conf_test.go index 81a3ed0..229be26 100644 --- a/pkg/wireguard/conf_test.go +++ b/pkg/wireguard/conf_test.go @@ -17,6 +17,8 @@ package wireguard import ( "net" "testing" + + "github.com/kylelemons/godebug/pretty" ) 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) + } + } +} diff --git a/pkg/wireguard/wireguard.go b/pkg/wireguard/wireguard.go index dbbba72..953eb9f 100644 --- a/pkg/wireguard/wireguard.go +++ b/pkg/wireguard/wireguard.go @@ -119,3 +119,15 @@ func ShowConf(iface string) ([]byte, error) { } 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 +}