mirror of
https://github.com/jwhited/wgsd.git
synced 2025-11-26 21:35:34 +08:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42d88f8049 | ||
|
|
f6140018d7 | ||
|
|
0bbd0d7e7f | ||
|
|
1cc63b3ad1 | ||
|
|
9dceead688 | ||
|
|
06de26a522 | ||
|
|
e7a5fe367e | ||
|
|
7940269f17 | ||
|
|
7eaacc000b | ||
|
|
d9845d72b8 | ||
|
|
734608346a | ||
|
|
e068f9d9d2 | ||
|
|
a700f38f3e | ||
|
|
77622af207 | ||
|
|
6f78170fbe | ||
|
|
7d03ee7041 | ||
|
|
a928f85a58 | ||
|
|
016a366d0f | ||
|
|
401ad4ea47 | ||
|
|
fd4b7d8879 | ||
|
|
ce787925be |
24
.github/workflows/build.yml
vendored
Normal file
24
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: build
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.19.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
|
||||
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@@ -1,9 +1,9 @@
|
||||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v2
|
||||
with:
|
||||
version: v1.29
|
||||
version: v1.46.2
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.19
|
||||
-
|
||||
name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -1,14 +1,14 @@
|
||||
name: test
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.14.x, 1.15.x]
|
||||
go-version: [1.19.x]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
@@ -22,8 +22,22 @@ builds:
|
||||
- CGO_ENABLED=0
|
||||
goos:
|
||||
- linux
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm
|
||||
- arm64
|
||||
|
||||
archives:
|
||||
- id: coredns
|
||||
format: tar.gz
|
||||
name_template: wgsd-coredns_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
|
||||
builds:
|
||||
- coredns
|
||||
|
||||
- id: wgsd-client
|
||||
format: tar.gz
|
||||
name_template: wgsd-client_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}
|
||||
builds:
|
||||
- wgsd-client
|
||||
|
||||
42
README.md
42
README.md
@@ -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
|
||||
|
||||
128
go.mod
128
go.mod
@@ -1,10 +1,128 @@
|
||||
module github.com/jwhited/wgsd
|
||||
|
||||
go 1.14
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/coredns/caddy v1.1.0
|
||||
github.com/coredns/coredns v1.8.0
|
||||
github.com/miekg/dns v1.1.34
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200511024508-91d9787b944f
|
||||
github.com/coredns/caddy v1.1.1
|
||||
github.com/coredns/coredns v1.10.0
|
||||
github.com/miekg/dns v1.1.50
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20221104135756-97bc4ad4a1cb
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute v1.7.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go v66.0.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest v0.11.28 // 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.2.1 // 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/antonmedv/expr v1.9.0 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/aws/aws-sdk-go v1.44.95 // 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/v3 v3.8.0 // 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.3 // 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.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.1.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.4.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.2 // 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.8.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
|
||||
github.com/philhofer/fwd v1.1.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/prometheus/client_golang v1.13.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // 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.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/term v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220920152132-bb719d3a6e2c // indirect
|
||||
google.golang.org/api v0.95.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f // indirect
|
||||
google.golang.org/grpc v1.49.0 // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/DataDog/dd-trace-go.v1 v1.41.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/api v0.25.0 // indirect
|
||||
k8s.io/apimachinery v0.25.0 // indirect
|
||||
k8s.io/client-go v0.24.4 // indirect
|
||||
k8s.io/klog/v2 v2.80.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
|
||||
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
101
setup.go
101
setup.go
@@ -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
|
||||
}
|
||||
|
||||
139
setup_test.go
139
setup_test.go
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
327
wgsd.go
327
wgsd.go
@@ -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
|
||||
}
|
||||
|
||||
176
wgsd_test.go
176
wgsd_test.go
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user