12 Commits

Author SHA1 Message Date
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 593 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` 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.
@@ -32,9 +35,20 @@ A basic client is available under [cmd/wgsd-client](cmd/wgsd-client).
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
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
@@ -42,7 +56,9 @@ This configuration:
```
$ cat Corefile
.: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
_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 extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda====._wireguard._udp.example.com.
$
$ 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====.example.com. 0 IN A 203.0.113.1
yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com. 0 IN SRV 0 0 7777 yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha====._wireguard._udp.example.com.
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
wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. 0 IN SRV 0 0 8888 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====.example.com.
wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====.example.com. 0 IN A 198.51.100.1
wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com. 0 IN SRV 0 0 8888 wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q====._wireguard._udp.example.com.
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:
@@ -88,9 +112,11 @@ $ echo yutrled535igkl7bdlerl6m4vjxsxm3uqqpl4nmsn27mt56ad4ha==== | tr '[:lower:]'
xScVkH3fUGUv4RrJFfmcqm8rs3SEHr41km6+yffAHw4=
$ echo wmrid55v4enhxqx2jstyoyvkicj5pihkb2tr7r42smiu3t5l4i5q==== | tr '[:lower:]' '[:upper:]' | base32 -d | base64
syKB97XhGnvC+kynh2KqQJPXoOoOpx/HmpMRTc+r4js=
$ echo extglt26a3znqnigvb5gvg26cqwblbgynf5re5pukdhx53cqwvda==== | tr '[:lower:]' '[:upper:]' | base32 -d | base64
JeZlz14G8tg1Bqh6apteFCwVhNhpexJ19FDPfuxQtUY=
```
## TODOs
- [x] unit tests
- [ ] SOA record support
- [x] CI & release binaries
- [x] CI & release binaries

101
setup.go
View File

@@ -2,6 +2,8 @@ package wgsd
import (
"fmt"
"net"
"strconv"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
@@ -11,45 +13,98 @@ import (
)
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 {
c.Next() // Ignore "wgsd" and give us the next token.
// return an error if there is no zone specified
if !c.NextArg() {
return plugin.Error("wgsd", c.ArgErr())
zones, err := parse(c)
if err != nil {
return plugin.Error(pluginName, err)
}
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()
if err != nil {
return plugin.Error("wgsd",
return plugin.Error(pluginName,
fmt.Errorf("error constructing wgctrl client: %v",
err))
}
c.OnFinalShutdown(client.Close)
// Add the Plugin to CoreDNS, so Servers can use it in their plugin chain.
dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler {
return &WGSD{
Next: next,
Zones: zones,
client: client,
zone: zone,
device: device,
}
})
return nil
}

View File

@@ -1,40 +1,167 @@
package wgsd
import (
"net"
"reflect"
"testing"
"github.com/coredns/caddy"
)
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 {
name string
input string
expectErr bool
name string
input string
shouldErr bool
expectedZones Zones
}{
{
"valid input",
"wgsd example.com. wg0",
false,
Zones{
Z: map[string]*Zone{
"example.com.": {
name: "example.com.",
device: "wg0",
},
},
Names: []string{"example.com."},
},
},
{
"missing token",
"wgsd example.com.",
true,
Zones{},
},
{
"too many tokens",
"wgsd example.com. wg0 extra",
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 {
t.Run(tc.name, func(t *testing.T) {
c := caddy.NewTestController("dns", tc.input)
err := setup(c)
if (err != nil) != tc.expectErr {
t.Fatalf("expectErr: %v, got err=%v", tc.expectErr, err)
zones, err := parse(c)
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)
}
}
})
}

321
wgsd.go
View File

@@ -3,6 +3,7 @@ package wgsd
import (
"context"
"encoding/base32"
"encoding/base64"
"fmt"
"net"
"strings"
@@ -15,19 +16,31 @@ import (
)
// 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.
type WGSD struct {
Next plugin.Handler
Zones
client wgctrlClient // the client for retrieving WireGuard peer information
}
// the client for retrieving Wireguard peer information
client wgctrlClient
// the DNS zone we are serving records for
zone string
// the Wireguard device name, e.g. wg0
device string
type Zones struct {
Z map[string]*Zone // a mapping from zone name to zone data
Names []string // all keys from the map z as a string slice
}
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 {
@@ -35,120 +48,186 @@ type wgctrlClient interface {
}
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."
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
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 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,
r *dns.Msg) (int, error) {
// request.Request is a convenience struct we wrap around the msg and
// ResponseWriter.
state := request.Request{W: w, Req: r}
// Check if the request is for the zone we are serving. If it doesn't match
// we pass the request on to the next plugin.
if plugin.Zones([]string{p.zone}).Matches(state.Name()) == "" {
// Check if the request is for a zone we are serving. If it doesn't match we
// pass the request on to the next plugin.
zoneName := plugin.Zones(p.Names).Matches(state.Name())
if zoneName == "" {
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
name := strings.TrimSuffix(state.Name(), p.zone)
qtype := state.QType()
name := strings.TrimSuffix(state.Name(), zoneName)
queryType := state.QType()
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 {
return dns.RcodeServerFailure, err
}
if len(device.Peers) == 0 {
return nxDomain(p.zone, w, r)
}
// setup our reply message
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)
}
return handler(state, peers)
}
func getHostRR(pubKey, zone string, endpoint *net.UDPAddr) dns.RR {
if endpoint == nil || endpoint.IP == nil {
return nil
}
name := fmt.Sprintf("%s.%s", strings.ToLower(pubKey), zone)
func getHostRR(name string, endpoint *net.UDPAddr) dns.RR {
switch {
case endpoint.IP.To4() != nil:
return &dns.A{
@@ -176,13 +255,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.SetReply(r)
m.SetReply(state.Req)
m.Authoritative = true
m.Rcode = dns.RcodeNameError
m.Ns = []dns.RR{soa(zone)}
w.WriteMsg(m) // nolint: errcheck
m.Ns = []dns.RR{soa(state.Zone)}
state.W.WriteMsg(m) // nolint: errcheck
return dns.RcodeSuccess, nil
}
@@ -205,5 +316,5 @@ func soa(zone string) dns.RR {
}
func (p *WGSD) Name() string {
return "wgsd"
return pluginName
}

View File

@@ -3,6 +3,7 @@ package wgsd
import (
"context"
"encoding/base32"
"encoding/base64"
"fmt"
"net"
"strings"
@@ -15,44 +16,85 @@ import (
)
type mockClient struct {
peers []wgtypes.Peer
devices map[string]*wgtypes.Device
}
func (m *mockClient) Device(d string) (*wgtypes.Device, error) {
return &wgtypes.Device{
Name: d,
Peers: m.peers,
}, nil
return m.devices[d], 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) {
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[0] = 1
peer1Allowed, peer1AllowedString := constructAllowedIPs(t, []string{"10.0.0.1/32", "10.0.0.2/32"})
peer1 := wgtypes.Peer{
Endpoint: &net.UDPAddr{
IP: net.ParseIP("1.1.1.1"),
Port: 1,
},
PublicKey: key1,
PublicKey: key1,
AllowedIPs: peer1Allowed,
}
peer1b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer1.PublicKey[:]))
peer1b64 := base64.StdEncoding.EncodeToString(peer1.PublicKey[:])
key2 := [32]byte{}
key2[0] = 2
peer2Allowed, peer2AllowedString := constructAllowedIPs(t, []string{"10.0.0.3/32", "10.0.0.4/32"})
peer2 := wgtypes.Peer{
Endpoint: &net.UDPAddr{
IP: net.ParseIP("::2"),
Port: 2,
},
PublicKey: key2,
PublicKey: key2,
AllowedIPs: peer2Allowed,
}
peer2b32 := strings.ToLower(base32.StdEncoding.EncodeToString(peer2.PublicKey[:]))
peer2b64 := base64.StdEncoding.EncodeToString(peer2.PublicKey[:])
p := &WGSD{
Next: test.ErrorHandler(),
client: &mockClient{
peers: []wgtypes.Peer{peer1, peer2},
Zones: Zones{
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},
},
},
},
zone: "example.com.",
device: "wg0",
}
testCases := []test.Case{
@@ -63,6 +105,19 @@ func TestWGSD(t *testing.T) {
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.", 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 +125,11 @@ func TestWGSD(t *testing.T) {
Qtype: dns.TypeSRV,
Rcode: dns.RcodeSuccess,
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{
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 +137,59 @@ func TestWGSD(t *testing.T) {
Qtype: dns.TypeSRV,
Rcode: dns.RcodeSuccess,
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{
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,
Rcode: dns.RcodeSuccess,
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,
Rcode: dns.RcodeSuccess,
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)),
},
},
{