13 Commits

Author SHA1 Message Date
Jordan Whited
7eaacc000b skip peers with nil endpoints 2021-01-18 15:12:30 -08:00
Jordan Whited
d9845d72b8 update README 2021-01-02 17:32:20 -08:00
Jordan Whited
734608346a update README 2021-01-02 17:31:00 -08:00
Jordan Whited
e068f9d9d2 support multiple zone:device mappings 2021-01-02 16:23:51 -08:00
Jordan Whited
a700f38f3e test self-allowed-ips and self-endpoint config parsing 2021-01-01 17:47:15 -08:00
Jordan Whited
77622af207 add configuration support for self overrides 2021-01-01 17:47:15 -08:00
Jordan Whited
6f78170fbe serve self peer info 2021-01-01 17:47:15 -08:00
Jordan Whited
7d03ee7041 standardize handler funcs 2021-01-01 17:47:15 -08:00
Jordan Whited
a928f85a58 serve allowed ips and public key via TXT RR 2020-12-31 14:18:33 -08:00
Jordan Whited
016a366d0f Update README for consistent name/target format 2020-12-29 13:22:25 -08:00
Benoît Ganne
401ad4ea47 always use full Service Instance Name
Service instance name is defined in RFC6763 section 4.1 as
  Service Instance Name = <Instance> . <Service> . <Domain>
Use it instead of <Instance> . <Domain> for consistency.
2020-12-29 13:15:48 -08:00
Julien Balestra
fd4b7d8879 setup: on shutdown, close the client connection 2020-12-23 11:13:14 -08:00
Jordan Whited
ce787925be add build workflow 2020-11-25 16:38:45 -08:00
6 changed files with 648 additions and 161 deletions

24
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: build
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
strategy:
matrix:
go-version: [1.14.x, 1.15.x]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout code
uses: actions/checkout@v2
- name: Build coredns
run: go build cmd/coredns/main.go
- name: Build wgsd-client
run: go build cmd/wgsd-client/main.go

View File

@@ -1,5 +1,8 @@
# wgsd # wgsd
`wgsd` is a [CoreDNS](https://github.com/coredns/coredns) plugin that serves WireGuard peer information via DNS-SD ([RFC6763](https://tools.ietf.org/html/rfc6763)) semantics. This enables dynamic discovery of WireGuard Endpoint addressing (both IP address and port number) with the added benefit of NAT-to-NAT WireGuard connectivity where [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) is supported. `wgsd` is a [CoreDNS](https://github.com/coredns/coredns) plugin that serves WireGuard peer information via DNS-SD ([RFC6763](https://tools.ietf.org/html/rfc6763)) semantics. This enables use cases such as:
* Building a mesh of WireGuard peers from a central registry
* Dynamic discovery of WireGuard Endpoint addressing (both IP address and port number)
* NAT-to-NAT WireGuard connectivity where [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) is supported.
See [this blog post](https://www.jordanwhited.com/posts/wireguard-endpoint-discovery-nat-traversal/) for a deep dive on the underlying techniques and development thought. See [this blog post](https://www.jordanwhited.com/posts/wireguard-endpoint-discovery-nat-traversal/) for a deep dive on the underlying techniques and development thought.
@@ -32,9 +35,20 @@ A basic client is available under [cmd/wgsd-client](cmd/wgsd-client).
wgsd ZONE DEVICE wgsd ZONE DEVICE
``` ```
* `ZONE` is the zone name wgsd should be authoritative for, e.g. example.com.
* `DEVICE` is the name of the WireGuard interface, e.g. wg0
```
wgsd ZONE DEVICE {
self [ ENDPOINT ] [ ALLOWED-IPS ... ]
}
```
* Supplying the `self` option enables serving data about the local WireGuard device in addition to its peers. The optional `ENDPOINT` argument enables setting a custom endpoint in ip:port form. If `ENDPOINT` is omitted wgsd will default to the local IP address for the DNS query and `ListenPort` of the WireGuard device. This can be useful if your host is behind NAT. The optional, variadic `ALLOWED-IPS` argument sets allowed-ips to be served for the local WireGuard device.
## Querying ## Querying
Following RFC6763 this plugin provides a listing of peers via PTR records at the namespace `_wireguard._udp.<zone>`. The target for the PTR records is `<base32PubKey>._wireguard._udp.<zone>` which corresponds to SRV records. SRV targets are of the format `<base32PubKey>.<zone>`. When querying the SRV record for a peer, the target A/AAAA records will be included in the "additional" section of the response. Public keys are represented in Base32 rather than Base64 to allow for their use in node names where they are treated as case-insensitive by the DNS. Following RFC6763 this plugin provides a listing of peers via PTR records at the namespace `_wireguard._udp.<zone>`. The target for the PTR records is of the format `<base32PubKey>._wireguard._udp.<zone>`. This same format is used for the accompanying SRV, A/AAAA, and TXT records. When querying the SRV record for a peer, the target A/AAAA & TXT records will be included in the "additional" section of the response. TXT records include Base64 public key and allowed IPs. Public keys are represented in Base32 rather than Base64 in record names as they are treated as case-insensitive by the DNS.
## Example ## Example
@@ -42,7 +56,9 @@ This configuration:
``` ```
$ cat Corefile $ cat Corefile
.:5353 { .:5353 {
wgsd example.com. wg0 wgsd example.com. wg0 {
self 192.0.2.1:51820 10.0.0.254/32
}
} }
``` ```
@@ -72,14 +88,22 @@ Will respond with:
$ dig @127.0.0.1 -p 5353 _wireguard._udp.example.com. PTR +noall +answer +additional $ dig @127.0.0.1 -p 5353 _wireguard._udp.example.com. PTR +noall +answer +additional
_wireguard._udp.example.com. 0 IN PTR yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. _wireguard._udp.example.com. 0 IN PTR yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com.
_wireguard._udp.example.com. 0 IN PTR wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. _wireguard._udp.example.com. 0 IN PTR wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com.
_wireguard._udp.example.com. 0 IN PTR extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com.
$ $
$ dig @127.0.0.1 -p 5353 yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. SRV +noall +answer +additional $ dig @127.0.0.1 -p 5353 yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. SRV +noall +answer +additional
yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. 0 IN SRV 0 0 7777 yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====.example.com. yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. 0 IN SRV 0 0 7777 yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com.
yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====.example.com. 0 IN A 203.0.113.1 yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. 0 IN A 203.0.113.1
yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. 0 IN TXT "txtvers=1" "pub=xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4=" "allowed=10.0.0.1/32"
$ $
$ dig @127.0.0.1 -p 5353 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. SRV +noall +answer +additional $ dig @127.0.0.1 -p 5353 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. SRV +noall +answer +additional
wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. 0 IN SRV 0 0 8888 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====.example.com. wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. 0 IN SRV 0 0 8888 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com.
wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====.example.com. 0 IN A 198.51.100.1 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. 0 IN A 198.51.100.1
wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. 0 IN TXT "txtvers=1" "pub=syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=" "allowed=10.0.0.2/32"
$
$ dig @127.0.0.1 -p 5353 extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com. SRV +noall +answer +additional
extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com. 0 IN SRV 0 0 51820 extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com.
extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com. 0 IN A 192.0.2.1
extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com. 0 IN TXT "txtvers=1" "pub=JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=" "allowed=10.0.0.254/32"
``` ```
Converting public keys to Base64 with coreutils: Converting public keys to Base64 with coreutils:
@@ -88,9 +112,11 @@ $ echo yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha==== | tr '[:lower:]'
xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4= xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4=
$ echo wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q==== | tr '[:lower:]' '[:upper:]' | base32 -d | base64 $ echo wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q==== | tr '[:lower:]' '[:upper:]' | base32 -d | base64
syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js= syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=
$ echo extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda==== | tr '[:lower:]' '[:upper:]' | base32 -d | base64
JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=
``` ```
## TODOs ## TODOs
- [x] unit tests - [x] unit tests
- [ ] SOA record support - [ ] SOA record support
- [x] CI & release binaries - [x] CI & release binaries

101
setup.go
View File

@@ -2,6 +2,8 @@ package wgsd
import ( import (
"fmt" "fmt"
"net"
"strconv"
"github.com/coredns/caddy" "github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/core/dnsserver"
@@ -11,45 +13,98 @@ import (
) )
func init() { func init() {
plugin.Register("wgsd", setup) plugin.Register(pluginName, setup)
}
func parse(c *caddy.Controller) (Zones, error) {
z := make(map[string]*Zone)
names := []string{}
for c.Next() {
// wgsd zone device
args := c.RemainingArgs()
if len(args) != 2 {
return Zones{}, fmt.Errorf("expected 2 args, got %d", len(args))
}
zone := &Zone{
name: dns.Fqdn(args[0]),
device: args[1],
}
names = append(names, zone.name)
_, ok := z[zone.name]
if ok {
return Zones{}, fmt.Errorf("duplicate zone name %s",
zone.name)
}
z[zone.name] = zone
for c.NextBlock() {
switch c.Val() {
case "self":
// self [endpoint] [allowed-ips ... ]
zone.serveSelf = true
args = c.RemainingArgs()
if len(args) < 1 {
break
}
// assume first arg is endpoint
host, portS, err := net.SplitHostPort(args[0])
if err == nil {
port, err := strconv.Atoi(portS)
if err != nil {
return Zones{}, fmt.Errorf("error converting self endpoint port: %v", err)
}
ip := net.ParseIP(host)
if ip == nil {
return Zones{}, fmt.Errorf("invalid self endpoint IP address: %s", host)
}
zone.selfEndpoint = &net.UDPAddr{
IP: ip,
Port: port,
}
args = args[1:]
}
if len(args) > 0 {
zone.selfAllowedIPs = make([]net.IPNet, 0)
}
for _, allowedIPString := range args {
_, prefix, err := net.ParseCIDR(allowedIPString)
if err != nil {
return Zones{}, fmt.Errorf("invalid self allowed-ip '%s' err: %v", allowedIPString, err)
}
zone.selfAllowedIPs = append(zone.selfAllowedIPs, *prefix)
}
default:
return Zones{}, c.ArgErr()
}
}
}
return Zones{Z: z, Names: names}, nil
} }
func setup(c *caddy.Controller) error { func setup(c *caddy.Controller) error {
c.Next() // Ignore "wgsd" and give us the next token. zones, err := parse(c)
if err != nil {
// return an error if there is no zone specified return plugin.Error(pluginName, err)
if !c.NextArg() {
return plugin.Error("wgsd", c.ArgErr())
} }
zone := dns.Fqdn(c.Val())
// return an error if there is no device name specified
if !c.NextArg() {
return plugin.Error("wgsd", c.ArgErr())
}
device := c.Val()
// return an error if there are more tokens on this line
if c.NextArg() {
return plugin.Error("wgsd", c.ArgErr())
}
client, err := wgctrl.New() client, err := wgctrl.New()
if err != nil { if err != nil {
return plugin.Error("wgsd", return plugin.Error(pluginName,
fmt.Errorf("error constructing wgctrl client: %v", fmt.Errorf("error constructing wgctrl client: %v",
err)) err))
} }
c.OnFinalShutdown(client.Close)
// Add the Plugin to CoreDNS, so Servers can use it in their plugin chain. // Add the Plugin to CoreDNS, so Servers can use it in their plugin chain.
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return &WGSD{ return &WGSD{
Next: next, Next: next,
Zones: zones,
client: client, client: client,
zone: zone,
device: device,
} }
}) })
return nil return nil
} }

View File

@@ -1,40 +1,167 @@
package wgsd package wgsd
import ( import (
"net"
"reflect"
"testing" "testing"
"github.com/coredns/caddy" "github.com/coredns/caddy"
) )
func TestSetup(t *testing.T) { func TestSetup(t *testing.T) {
_, prefix1, _ := net.ParseCIDR("1.1.1.1/32")
_, prefix2, _ := net.ParseCIDR("2.2.2.2/32")
_, prefix3, _ := net.ParseCIDR("3.3.3.3/32")
_, prefix4, _ := net.ParseCIDR("4.4.4.4/32")
endpoint1 := &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 51820}
testCases := []struct { testCases := []struct {
name string name string
input string input string
expectErr bool shouldErr bool
expectedZones Zones
}{ }{
{ {
"valid input", "valid input",
"wgsd example.com. wg0", "wgsd example.com. wg0",
false, false,
Zones{
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
},
},
Names: []string{"example.com."},
},
}, },
{ {
"missing token", "missing token",
"wgsd example.com.", "wgsd example.com.",
true, true,
Zones{},
}, },
{ {
"too many tokens", "too many tokens",
"wgsd example.com. wg0 extra", "wgsd example.com. wg0 extra",
true, true,
Zones{},
},
{
"valid self allowed-ips",
`wgsd example.com. wg0 {
self 1.1.1.1/32 2.2.2.2/32
}`,
false,
Zones{
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
serveSelf: true,
selfAllowedIPs: []net.IPNet{*prefix1, *prefix2},
},
},
Names: []string{"example.com."},
},
},
{
"invalid self-allowed-ips",
`wgsd example.com. wg0 {
self 1.1.11/32 2.2.2.2/32
}`,
true,
Zones{},
},
{
"valid self-endpoint",
`wgsd example.com. wg0 {
self 127.0.0.1:51820
}`,
false,
Zones{
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
serveSelf: true,
selfEndpoint: endpoint1,
},
},
Names: []string{"example.com."},
},
},
{
"invalid self-endpoint",
`wgsd example.com. wg0 {
self hostname:51820
}`,
true,
Zones{},
},
{
"multiple blocks",
`wgsd example.com. wg0 {
self 127.0.0.1:51820 1.1.1.1/32 2.2.2.2/32
}
wgsd example2.com. wg1 {
self 127.0.0.1:51820 3.3.3.3/32 4.4.4.4/32
}`,
false,
Zones{
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
serveSelf: true,
selfEndpoint: endpoint1,
selfAllowedIPs: []net.IPNet{*prefix1, *prefix2},
},
"example2.com.": {
name: "example2.com.",
device: "wg1",
serveSelf: true,
selfEndpoint: endpoint1,
selfAllowedIPs: []net.IPNet{*prefix3, *prefix4},
},
},
Names: []string{"example.com.", "example2.com."},
},
},
{
"all options",
`wgsd example.com. wg0 {
self 127.0.0.1:51820 1.1.1.1/32 2.2.2.2/32
}`,
false,
Zones{
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
serveSelf: true,
selfEndpoint: endpoint1,
selfAllowedIPs: []net.IPNet{*prefix1, *prefix2},
},
},
Names: []string{"example.com."},
},
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
c := caddy.NewTestController("dns", tc.input) c := caddy.NewTestController("dns", tc.input)
err := setup(c) zones, err := parse(c)
if (err != nil) != tc.expectErr {
t.Fatalf("expectErr: %v, got err=%v", tc.expectErr, err) if err == nil && tc.shouldErr {
t.Fatal("expected errors, but got no error")
} else if err != nil && !tc.shouldErr {
t.Fatalf("expected no errors, but got '%v'", err)
} else {
if !reflect.DeepEqual(tc.expectedZones, zones) {
t.Fatalf("expected %v, got %v", tc.expectedZones, zones)
}
} }
}) })
} }

327
wgsd.go
View File

@@ -3,6 +3,7 @@ package wgsd
import ( import (
"context" "context"
"encoding/base32" "encoding/base32"
"encoding/base64"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@@ -15,19 +16,31 @@ import (
) )
// coredns plugin-specific logger // coredns plugin-specific logger
var logger = clog.NewWithPlugin("wgsd") var logger = clog.NewWithPlugin(pluginName)
// WGSD is a CoreDNS plugin that provides Wireguard peer information via DNS-SD const (
pluginName = "wgsd"
)
// WGSD is a CoreDNS plugin that provides WireGuard peer information via DNS-SD
// semantics. WGSD implements the plugin.Handler interface. // semantics. WGSD implements the plugin.Handler interface.
type WGSD struct { type WGSD struct {
Next plugin.Handler Next plugin.Handler
Zones
client wgctrlClient // the client for retrieving WireGuard peer information
}
// the client for retrieving Wireguard peer information type Zones struct {
client wgctrlClient Z map[string]*Zone // a mapping from zone name to zone data
// the DNS zone we are serving records for Names []string // all keys from the map z as a string slice
zone string }
// the Wireguard device name, e.g. wg0
device string type Zone struct {
name string // the name of the zone we are authoritative for
device string // the WireGuard device name, e.g. wg0
serveSelf bool // flag to enable serving data about self
selfEndpoint *net.UDPAddr // overrides the self endpoint value
selfAllowedIPs []net.IPNet // self allowed IPs
} }
type wgctrlClient interface { type wgctrlClient interface {
@@ -35,120 +48,192 @@ type wgctrlClient interface {
} }
const ( const (
keyLen = 56 // the number of characters in a base32-encoded Wireguard public key keyLen = 56 // the number of characters in a base32-encoded WireGuard public key
spPrefix = "_wireguard._udp." spPrefix = "_wireguard._udp."
serviceInstanceLen = keyLen + len(".") + len(spPrefix) spSubPrefix = "." + spPrefix
serviceInstanceLen = keyLen + len(spSubPrefix)
) )
type handlerFn func(state request.Request, peers []wgtypes.Peer) (int, error)
func getHandlerFn(queryType uint16, name string) handlerFn {
switch {
case name == spPrefix && queryType == dns.TypePTR:
return handlePTR
case len(name) == serviceInstanceLen && queryType == dns.TypeSRV:
return handleSRV
case len(name) == len(spSubPrefix)+keyLen && (queryType == dns.TypeA ||
queryType == dns.TypeAAAA || queryType == dns.TypeTXT):
return handleHostOrTXT
default:
return nil
}
}
func handlePTR(state request.Request, peers []wgtypes.Peer) (int, error) {
m := new(dns.Msg)
m.SetReply(state.Req)
m.Authoritative = true
for _, peer := range peers {
if peer.Endpoint == nil {
continue
}
m.Answer = append(m.Answer, &dns.PTR{
Hdr: dns.RR_Header{
Name: state.Name(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: 0,
},
Ptr: fmt.Sprintf("%s.%s%s",
strings.ToLower(base32.StdEncoding.EncodeToString(peer.PublicKey[:])),
spPrefix, state.Zone),
})
}
state.W.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
}
func handleSRV(state request.Request, peers []wgtypes.Peer) (int, error) {
m := new(dns.Msg)
m.SetReply(state.Req)
m.Authoritative = true
pubKey := state.Name()[:keyLen]
for _, peer := range peers {
if strings.EqualFold(
base32.StdEncoding.EncodeToString(peer.PublicKey[:]), pubKey) {
endpoint := peer.Endpoint
if endpoint == nil {
return nxDomain(state)
}
hostRR := getHostRR(state.Name(), endpoint)
if hostRR == nil {
return nxDomain(state)
}
txtRR := getTXTRR(state.Name(), peer)
m.Extra = append(m.Extra, hostRR, txtRR)
m.Answer = append(m.Answer, &dns.SRV{
Hdr: dns.RR_Header{
Name: state.Name(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: 0,
},
Priority: 0,
Weight: 0,
Port: uint16(endpoint.Port),
Target: state.Name(),
})
state.W.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
}
}
return nxDomain(state)
}
func handleHostOrTXT(state request.Request, peers []wgtypes.Peer) (int, error) {
m := new(dns.Msg)
m.SetReply(state.Req)
m.Authoritative = true
pubKey := state.Name()[:keyLen]
for _, peer := range peers {
if strings.EqualFold(
base32.StdEncoding.EncodeToString(peer.PublicKey[:]), pubKey) {
endpoint := peer.Endpoint
if endpoint == nil {
return nxDomain(state)
}
if state.QType() == dns.TypeA || state.QType() == dns.TypeAAAA {
hostRR := getHostRR(state.Name(), endpoint)
if hostRR == nil {
return nxDomain(state)
}
m.Answer = append(m.Answer, hostRR)
} else {
txtRR := getTXTRR(state.Name(), peer)
m.Answer = append(m.Answer, txtRR)
}
state.W.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
}
}
return nxDomain(state)
}
func getSelfPeer(zone *Zone, device *wgtypes.Device, state request.Request) (wgtypes.Peer, error) {
self := wgtypes.Peer{
PublicKey: device.PublicKey,
}
if zone.selfEndpoint != nil {
self.Endpoint = zone.selfEndpoint
} else {
self.Endpoint = &net.UDPAddr{
IP: net.ParseIP(state.LocalIP()),
Port: device.ListenPort,
}
}
self.AllowedIPs = zone.selfAllowedIPs
return self, nil
}
func getPeers(client wgctrlClient, zone *Zone, state request.Request) (
[]wgtypes.Peer, error) {
peers := make([]wgtypes.Peer, 0)
device, err := client.Device(zone.device)
if err != nil {
return nil, err
}
peers = append(peers, device.Peers...)
if zone.serveSelf {
self, err := getSelfPeer(zone, device, state)
if err != nil {
return nil, err
}
peers = append(peers, self)
}
return peers, nil
}
func (p *WGSD) ServeDNS(ctx context.Context, w dns.ResponseWriter, func (p *WGSD) ServeDNS(ctx context.Context, w dns.ResponseWriter,
r *dns.Msg) (int, error) { r *dns.Msg) (int, error) {
// request.Request is a convenience struct we wrap around the msg and // request.Request is a convenience struct we wrap around the msg and
// ResponseWriter. // ResponseWriter.
state := request.Request{W: w, Req: r} state := request.Request{W: w, Req: r}
// Check if the request is for the zone we are serving. If it doesn't match // Check if the request is for a zone we are serving. If it doesn't match we
// we pass the request on to the next plugin. // pass the request on to the next plugin.
if plugin.Zones([]string{p.zone}).Matches(state.Name()) == "" { zoneName := plugin.Zones(p.Names).Matches(state.Name())
if zoneName == "" {
return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r) return plugin.NextOrFailure(p.Name(), p.Next, ctx, w, r)
} }
state.Zone = zoneName
zone, ok := p.Z[zoneName]
if !ok {
return dns.RcodeServerFailure, nil
}
// strip zone from name // strip zone from name
name := strings.TrimSuffix(state.Name(), p.zone) name := strings.TrimSuffix(state.Name(), zoneName)
qtype := state.QType() queryType := state.QType()
logger.Debugf("received query for: %s type: %s", name, logger.Debugf("received query for: %s type: %s", name,
dns.TypeToString[qtype]) dns.TypeToString[queryType])
device, err := p.client.Device(p.device) handler := getHandlerFn(queryType, name)
if handler == nil {
return nxDomain(state)
}
peers, err := getPeers(p.client, zone, state)
if err != nil { if err != nil {
return dns.RcodeServerFailure, err return dns.RcodeServerFailure, err
} }
if len(device.Peers) == 0 {
return nxDomain(p.zone, w, r)
}
// setup our reply message return handler(state, peers)
m := new(dns.Msg)
m.SetReply(r)
m.Authoritative = true
switch {
// TODO: handle SOA
case name == spPrefix && qtype == dns.TypePTR:
for _, peer := range device.Peers {
if peer.Endpoint == nil {
continue
}
m.Answer = append(m.Answer, &dns.PTR{
Hdr: dns.RR_Header{
Name: state.Name(),
Rrtype: dns.TypePTR,
Class: dns.ClassINET,
Ttl: 0,
},
Ptr: fmt.Sprintf("%s.%s%s",
strings.ToLower(base32.StdEncoding.EncodeToString(peer.PublicKey[:])),
spPrefix, p.zone),
})
}
w.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
case len(name) == serviceInstanceLen && qtype == dns.TypeSRV:
pubKey := name[:keyLen]
for _, peer := range device.Peers {
if strings.EqualFold(
base32.StdEncoding.EncodeToString(peer.PublicKey[:]), pubKey) {
endpoint := peer.Endpoint
hostRR := getHostRR(pubKey, p.zone, endpoint)
if hostRR == nil {
return nxDomain(p.zone, w, r)
}
m.Extra = append(m.Extra, hostRR)
m.Answer = append(m.Answer, &dns.SRV{
Hdr: dns.RR_Header{
Name: state.Name(),
Rrtype: dns.TypeSRV,
Class: dns.ClassINET,
Ttl: 0,
},
Priority: 0,
Weight: 0,
Port: uint16(endpoint.Port),
Target: fmt.Sprintf("%s.%s",
strings.ToLower(pubKey), p.zone),
})
w.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
}
}
return nxDomain(p.zone, w, r)
case len(name) == keyLen+1 && (qtype == dns.TypeA ||
qtype == dns.TypeAAAA):
pubKey := name[:keyLen]
for _, peer := range device.Peers {
if strings.EqualFold(
base32.StdEncoding.EncodeToString(peer.PublicKey[:]), pubKey) {
endpoint := peer.Endpoint
hostRR := getHostRR(pubKey, p.zone, endpoint)
if hostRR == nil {
return nxDomain(p.zone, w, r)
}
m.Answer = append(m.Answer, hostRR)
w.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
}
}
return nxDomain(p.zone, w, r)
default:
return nxDomain(p.zone, w, r)
}
} }
func getHostRR(pubKey, zone string, endpoint *net.UDPAddr) dns.RR { func getHostRR(name string, endpoint *net.UDPAddr) dns.RR {
if endpoint == nil || endpoint.IP == nil {
return nil
}
name := fmt.Sprintf("%s.%s", strings.ToLower(pubKey), zone)
switch { switch {
case endpoint.IP.To4() != nil: case endpoint.IP.To4() != nil:
return &dns.A{ return &dns.A{
@@ -176,13 +261,45 @@ func getHostRR(pubKey, zone string, endpoint *net.UDPAddr) dns.RR {
} }
} }
func nxDomain(zone string, w dns.ResponseWriter, r *dns.Msg) (int, error) { const (
// txtVersion is the first key/value pair in the TXT RR. Its serves to aid
// clients with maintaining backwards compatibility.
//
// https://tools.ietf.org/html/rfc6763#section-6.7
txtVersion = 1
)
func getTXTRR(name string, peer wgtypes.Peer) *dns.TXT {
var allowedIPs string
for i, prefix := range peer.AllowedIPs {
if i != 0 {
allowedIPs += ","
}
allowedIPs += prefix.String()
}
return &dns.TXT{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeTXT,
Class: dns.ClassINET,
Ttl: 0,
},
Txt: []string{
fmt.Sprintf("txtvers=%d", txtVersion),
fmt.Sprintf("pub=%s",
base64.StdEncoding.EncodeToString(peer.PublicKey[:])),
fmt.Sprintf("allowed=%s", allowedIPs),
},
}
}
func nxDomain(state request.Request) (int, error) {
m := new(dns.Msg) m := new(dns.Msg)
m.SetReply(r) m.SetReply(state.Req)
m.Authoritative = true m.Authoritative = true
m.Rcode = dns.RcodeNameError m.Rcode = dns.RcodeNameError
m.Ns = []dns.RR{soa(zone)} m.Ns = []dns.RR{soa(state.Zone)}
w.WriteMsg(m) // nolint: errcheck state.W.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil return dns.RcodeSuccess, nil
} }
@@ -205,5 +322,5 @@ func soa(zone string) dns.RR {
} }
func (p *WGSD) Name() string { func (p *WGSD) Name() string {
return "wgsd" return pluginName
} }

View File

@@ -3,6 +3,7 @@ package wgsd
import ( import (
"context" "context"
"encoding/base32" "encoding/base32"
"encoding/base64"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@@ -15,44 +16,94 @@ import (
) )
type mockClient struct { type mockClient struct {
peers []wgtypes.Peer devices map[string]*wgtypes.Device
} }
func (m *mockClient) Device(d string) (*wgtypes.Device, error) { func (m *mockClient) Device(d string) (*wgtypes.Device, error) {
return &wgtypes.Device{ return m.devices[d], nil
Name: d, }
Peers: m.peers,
}, nil func constructAllowedIPs(t *testing.T, prefixes []string) ([]net.IPNet, string) {
var allowed []net.IPNet
var allowedString string
for i, s := range prefixes {
_, prefix, err := net.ParseCIDR(s)
if err != nil {
t.Fatalf("error parsing cidr: %v", err)
}
allowed = append(allowed, *prefix)
if i != 0 {
allowedString += ","
}
allowedString += prefix.String()
}
return allowed, allowedString
} }
func TestWGSD(t *testing.T) { func TestWGSD(t *testing.T) {
selfKey := [32]byte{}
selfKey[0] = 99
selfb32 := strings.ToLower(base32.StdEncoding.EncodeToString(selfKey[:]))
selfb64 := base64.StdEncoding.EncodeToString(selfKey[:])
selfAllowed, selfAllowedString := constructAllowedIPs(t, []string{"10.0.0.99/32", "10.0.0.100/32"})
key1 := [32]byte{} key1 := [32]byte{}
key1[0] = 1 key1[0] = 1
peer1Allowed, peer1AllowedString := constructAllowedIPs(t, []string{"10.0.0.1/32", "10.0.0.2/32"})
peer1 := wgtypes.Peer{ peer1 := wgtypes.Peer{
Endpoint: &net.UDPAddr{ Endpoint: &net.UDPAddr{
IP: net.ParseIP("1.1.1.1"), IP: net.ParseIP("1.1.1.1"),
Port: 1, Port: 1,
}, },
PublicKey: key1, PublicKey: key1,
AllowedIPs: peer1Allowed,
} }
peer1b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer1.PublicKey[:])) peer1b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer1.PublicKey[:]))
peer1b64 := base64.StdEncoding.EncodeToString(peer1.PublicKey[:])
key2 := [32]byte{} key2 := [32]byte{}
key2[0] = 2 key2[0] = 2
peer2Allowed, peer2AllowedString := constructAllowedIPs(t, []string{"10.0.0.3/32", "10.0.0.4/32"})
peer2 := wgtypes.Peer{ peer2 := wgtypes.Peer{
Endpoint: &net.UDPAddr{ Endpoint: &net.UDPAddr{
IP: net.ParseIP("::2"), IP: net.ParseIP("::2"),
Port: 2, Port: 2,
}, },
PublicKey: key2, PublicKey: key2,
AllowedIPs: peer2Allowed,
} }
peer2b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer2.PublicKey[:])) peer2b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer2.PublicKey[:]))
peer2b64 := base64.StdEncoding.EncodeToString(peer2.PublicKey[:])
key3 := [32]byte{}
key3[0] = 3
peer3Allowed, _ := constructAllowedIPs(t, []string{"10.0.0.5/32", "10.0.0.6/32"})
peer3 := wgtypes.Peer{
Endpoint: nil,
PublicKey: key3,
AllowedIPs: peer3Allowed,
}
peer3b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer3.PublicKey[:]))
p := &WGSD{ p := &WGSD{
Next: test.ErrorHandler(), Next: test.ErrorHandler(),
client: &mockClient{ Zones: Zones{
peers: []wgtypes.Peer{peer1, peer2}, Names: []string{"example.com."},
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
serveSelf: true,
selfAllowedIPs: selfAllowed,
},
},
},
client: &mockClient{
devices: map[string]*wgtypes.Device{
"wg0": {
Name: "wg0",
PublicKey: selfKey,
ListenPort: 51820,
Peers: []wgtypes.Peer{peer1, peer2, peer3},
},
},
}, },
zone: "example.com.",
device: "wg0",
} }
testCases := []test.Case{ testCases := []test.Case{
@@ -63,6 +114,19 @@ func TestWGSD(t *testing.T) {
Answer: []dns.RR{ Answer: []dns.RR{
test.PTR(fmt.Sprintf("_wireguard._udp.example.com. 0 IN PTR %s._wireguard._udp.example.com.", peer1b32)), test.PTR(fmt.Sprintf("_wireguard._udp.example.com. 0 IN PTR %s._wireguard._udp.example.com.", peer1b32)),
test.PTR(fmt.Sprintf("_wireguard._udp.example.com. 0 IN PTR %s._wireguard._udp.example.com.", peer2b32)), test.PTR(fmt.Sprintf("_wireguard._udp.example.com. 0 IN PTR %s._wireguard._udp.example.com.", peer2b32)),
test.PTR(fmt.Sprintf("_wireguard._udp.example.com. 0 IN PTR %s._wireguard._udp.example.com.", selfb32)),
},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", selfb32),
Qtype: dns.TypeSRV,
Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.SRV(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN SRV 0 0 51820 %s._wireguard._udp.example.com.", selfb32, selfb32)),
},
Extra: []dns.RR{
test.A(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN A %s", selfb32, "127.0.0.1")),
test.TXT(fmt.Sprintf(`%s._wireguard._udp.example.com. 0 IN TXT "txtvers=%d" "pub=%s" "allowed=%s"`, selfb32, txtVersion, selfb64, selfAllowedString)),
}, },
}, },
{ {
@@ -70,10 +134,11 @@ func TestWGSD(t *testing.T) {
Qtype: dns.TypeSRV, Qtype: dns.TypeSRV,
Rcode: dns.RcodeSuccess, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{ Answer: []dns.RR{
test.SRV(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN SRV 0 0 1 %s.example.com.", peer1b32, peer1b32)), test.SRV(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN SRV 0 0 1 %s._wireguard._udp.example.com.", peer1b32, peer1b32)),
}, },
Extra: []dns.RR{ Extra: []dns.RR{
test.A(fmt.Sprintf("%s.example.com. 0 IN A %s", peer1b32, peer1.Endpoint.IP.String())), test.A(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN A %s", peer1b32, peer1.Endpoint.IP.String())),
test.TXT(fmt.Sprintf(`%s._wireguard._udp.example.com. 0 IN TXT "txtvers=%d" "pub=%s" "allowed=%s"`, peer1b32, txtVersion, peer1b64, peer1AllowedString)),
}, },
}, },
{ {
@@ -81,26 +146,59 @@ func TestWGSD(t *testing.T) {
Qtype: dns.TypeSRV, Qtype: dns.TypeSRV,
Rcode: dns.RcodeSuccess, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{ Answer: []dns.RR{
test.SRV(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN SRV 0 0 2 %s.example.com.", peer2b32, peer2b32)), test.SRV(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN SRV 0 0 2 %s._wireguard._udp.example.com.", peer2b32, peer2b32)),
}, },
Extra: []dns.RR{ Extra: []dns.RR{
test.AAAA(fmt.Sprintf("%s.example.com. 0 IN AAAA %s", peer2b32, peer2.Endpoint.IP.String())), test.AAAA(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN AAAA %s", peer2b32, peer2.Endpoint.IP.String())),
test.TXT(fmt.Sprintf(`%s._wireguard._udp.example.com. 0 IN TXT "txtvers=%d" "pub=%s" "allowed=%s"`, peer2b32, txtVersion, peer2b64, peer2AllowedString)),
}, },
}, },
{ {
Qname: fmt.Sprintf("%s.example.com.", peer1b32), Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", selfb32),
Qtype: dns.TypeA, Qtype: dns.TypeA,
Rcode: dns.RcodeSuccess, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{ Answer: []dns.RR{
test.A(fmt.Sprintf("%s.example.com. 0 IN A %s", peer1b32, peer1.Endpoint.IP.String())), test.A(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN A %s", selfb32, "127.0.0.1")),
}, },
}, },
{ {
Qname: fmt.Sprintf("%s.example.com.", peer2b32), Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer1b32),
Qtype: dns.TypeA,
Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.A(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN A %s", peer1b32, peer1.Endpoint.IP.String())),
},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer2b32),
Qtype: dns.TypeAAAA, Qtype: dns.TypeAAAA,
Rcode: dns.RcodeSuccess, Rcode: dns.RcodeSuccess,
Answer: []dns.RR{ Answer: []dns.RR{
test.AAAA(fmt.Sprintf("%s.example.com. 0 IN AAAA %s", peer2b32, peer2.Endpoint.IP.String())), test.AAAA(fmt.Sprintf("%s._wireguard._udp.example.com. 0 IN AAAA %s", peer2b32, peer2.Endpoint.IP.String())),
},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", selfb32),
Qtype: dns.TypeTXT,
Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.TXT(fmt.Sprintf(`%s._wireguard._udp.example.com. 0 IN TXT "txtvers=%d" "pub=%s" "allowed=%s"`, selfb32, txtVersion, selfb64, selfAllowedString)),
},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer1b32),
Qtype: dns.TypeTXT,
Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.TXT(fmt.Sprintf(`%s._wireguard._udp.example.com. 0 IN TXT "txtvers=%d" "pub=%s" "allowed=%s"`, peer1b32, txtVersion, peer1b64, peer1AllowedString)),
},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer2b32),
Qtype: dns.TypeTXT,
Rcode: dns.RcodeSuccess,
Answer: []dns.RR{
test.TXT(fmt.Sprintf(`%s._wireguard._udp.example.com. 0 IN TXT "txtvers=%d" "pub=%s" "allowed=%s"`, peer2b32, txtVersion, peer2b64, peer2AllowedString)),
}, },
}, },
{ {
@@ -116,6 +214,46 @@ func TestWGSD(t *testing.T) {
Qtype: dns.TypeAAAA, Qtype: dns.TypeAAAA,
Rcode: dns.RcodeServerFailure, Rcode: dns.RcodeServerFailure,
}, },
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer3b32),
Qtype: dns.TypeSRV,
Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA(soa("example.com.").String()),
},
Answer: []dns.RR{},
Extra: []dns.RR{},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer3b32),
Qtype: dns.TypeA,
Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA(soa("example.com.").String()),
},
Answer: []dns.RR{},
Extra: []dns.RR{},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer3b32),
Qtype: dns.TypeAAAA,
Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA(soa("example.com.").String()),
},
Answer: []dns.RR{},
Extra: []dns.RR{},
},
{
Qname: fmt.Sprintf("%s._wireguard._udp.example.com.", peer3b32),
Qtype: dns.TypeTXT,
Rcode: dns.RcodeNameError,
Ns: []dns.RR{
test.SOA(soa("example.com.").String()),
},
Answer: []dns.RR{},
Extra: []dns.RR{},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(fmt.Sprintf("%s %s", tc.Qname, dns.TypeToString[tc.Qtype]), func(t *testing.T) { t.Run(fmt.Sprintf("%s %s", tc.Qname, dns.TypeToString[tc.Qtype]), func(t *testing.T) {