16 Commits

Author SHA1 Message Date
Jordan Whited
e7a5fe367e update release workflow to build with go1.17 2022-05-18 10:48:44 -07:00
Jordan Whited
7940269f17 upgrade to coredns v1.9.2 2022-05-18 10:42:34 -07:00
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
Jordan Whited
dfd8346f69 update import paths to support coredns v1.8.0 2020-11-24 15:05:19 -08:00
11 changed files with 2009 additions and 472 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.17.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

@@ -14,4 +14,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v2
with:
version: v1.29
version: v1.46.2

View File

@@ -18,7 +18,7 @@ jobs:
name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.15
go-version: 1.17
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2

View File

@@ -8,7 +8,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.14.x, 1.15.x]
go-version: [1.17.x]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:

View File

@@ -1,9 +1,19 @@
# 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.
## Installation
Binary releases are available [here](https://github.com/jwhited/wgsd/releases).
Each release contains 2 binaries:
* `coredns` - CoreDNS server with all the "internal" plugins + `wgsd`
* `wgsd-client` - A sample client
## Building from source
External CoreDNS plugins can be enabled in one of two ways:
1. [Build with compile-time configuration file](https://coredns.io/2017/07/25/compile-time-enabling-or-disabling-plugins/#build-with-compile-time-configuration-file)
@@ -25,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
@@ -35,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
}
}
```
@@ -65,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:
@@ -81,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
- [ ] CI & release binaries
- [x] CI & release binaries

126
go.mod
View File

@@ -1,10 +1,126 @@
module github.com/jwhited/wgsd
go 1.14
go 1.17
require (
github.com/caddyserver/caddy v1.0.5
github.com/coredns/coredns v1.6.9
github.com/miekg/dns v1.1.29
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200511024508-91d9787b944f
github.com/coredns/caddy v1.1.1
github.com/coredns/coredns v1.9.2
github.com/miekg/dns v1.1.48
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
)
require (
cloud.google.com/go/compute v1.6.1 // indirect
github.com/Azure/azure-sdk-for-go v63.4.0+incompatible // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.27 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.2.0 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/DataDog/datadog-agent/pkg/obfuscate v0.0.0-20211129110424-6491aa3bf583 // indirect
github.com/DataDog/datadog-go v4.8.2+incompatible // indirect
github.com/DataDog/datadog-go/v5 v5.0.2 // indirect
github.com/DataDog/sketches-go v1.0.0 // indirect
github.com/Microsoft/go-winio v0.5.1 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.44.9 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgraph-io/ristretto v0.1.0 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/dnstap/golang-dnstap v0.4.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/emicklei/go-restful v2.9.5+incompatible // indirect
github.com/farsightsec/golang-framestream v0.3.0 // indirect
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/go-logr/logr v1.2.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.3.0 // indirect
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/infobloxopen/go-trees v0.0.0-20200715205103-96a057b8dfb9 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/josharian/native v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mdlayher/genetlink v1.2.0 // indirect
github.com/mdlayher/netlink v1.6.0 // indirect
github.com/mdlayher/socket v0.2.3 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5 // indirect
github.com/openzipkin/zipkin-go v0.4.0 // indirect
github.com/oschwald/geoip2-golang v1.7.0 // indirect
github.com/oschwald/maxminddb-golang v1.9.0 // indirect
github.com/philhofer/fwd v1.1.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.34.0 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tinylib/msgp v1.1.2 // indirect
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
go.etcd.io/etcd/client/v3 v3.5.4 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/mod v0.4.2 // indirect
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d // indirect
google.golang.org/api v0.78.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e // indirect
google.golang.org/grpc v1.46.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/DataDog/dd-trace-go.v1 v1.38.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
k8s.io/api v0.24.0 // indirect
k8s.io/apimachinery v0.24.0 // indirect
k8s.io/client-go v0.24.0 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/kube-openapi v0.0.0-20220328201542-3ee0da9b0b42 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
sigs.k8s.io/yaml v1.2.0 // indirect
)

1529
go.sum

File diff suppressed because it is too large Load Diff

103
setup.go
View File

@@ -2,8 +2,10 @@ package wgsd
import (
"fmt"
"net"
"strconv"
"github.com/caddyserver/caddy"
"github.com/coredns/caddy"
"github.com/coredns/coredns/core/dnsserver"
"github.com/coredns/coredns/plugin"
"github.com/miekg/dns"
@@ -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/caddyserver/caddy"
"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)
}
}
})
}

327
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,192 @@ 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
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,
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 +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.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 +322,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,94 @@ 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[:])
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{
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, peer3},
},
},
},
zone: "example.com.",
device: "wg0",
}
testCases := []test.Case{
@@ -63,6 +114,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 +134,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 +146,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)),
},
},
{
@@ -116,6 +214,46 @@ func TestWGSD(t *testing.T) {
Qtype: dns.TypeAAAA,
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 {
t.Run(fmt.Sprintf("%s %s", tc.Qname, dns.TypeToString[tc.Qtype]), func(t *testing.T) {