mirror of
https://github.com/cunnie/sslip.io.git
synced 2025-09-27 12:02:14 +08:00
Nuked: procuring a wildcard certificate
The documentation on how to procure a wildcard certificate had gotten overly-complicated and stale, the Docker image, old, and the code, even older. Perhaps more importantly I couldn't bring myself to care whether people could procure a wildcard certificate. Signed-off-by: Brian Cunnie <brian.cunnie@gmail.com>
This commit is contained in:
@@ -127,9 +127,7 @@ as ARM64 (AWS Graviton, Apple M1/M2).
|
||||
`go run main.go -nameservers ns1.example.com,ns2.example.com`). If you're
|
||||
running your own nameservers, you probably want to set this. Don't forget to
|
||||
set address records for the new name servers with the `-addresses` flag (see
|
||||
below). Exception: `_acme-challenge` records are handled differently to
|
||||
accommodate the procurement of Let's Encrypt wildcard certificates; you can
|
||||
read more about that procedure [here](docs/wildcard.md)
|
||||
below).
|
||||
- `-addresses` overrides the default A/AAAA (IPv4/IPv6) address records. For
|
||||
example, here's how we set the IPv4 record & IPv6 record for our nameserver
|
||||
(in the `-nameservers` example above), ns1.example.com: `-addresses
|
||||
|
194
docs/wildcard.md
194
docs/wildcard.md
@@ -1,194 +0,0 @@
|
||||
## Procuring a Wildcard Certificate
|
||||
|
||||
### Using a White Label Domain
|
||||
|
||||
Let's say you have a domain that is hosted on Amazon Route53, lets call it
|
||||
`example.com`. You have a few DNS entries set up like `foo.example.com`, and then
|
||||
you have `xip.example.com` which is an NS record to `ns-ovh.sslip.io`. So you
|
||||
are able to use both regular DNS records that are hardcoded, and then when you
|
||||
need to use sslip you simply use your xip subdomain.
|
||||
|
||||
To get a wildcard certificate for `*.xip.example.com`, simply go through the regular
|
||||
Let's Encrypt DNS-01 challenge process.
|
||||
|
||||
Let's Encrypt will query your name servers for the TXT record
|
||||
`_acme-challenge.xip.example.com`, then your DNS server will respond with the
|
||||
TXT record _that should have been created on Route53 as part of the challenge_,
|
||||
otherwise it'll return the delegated nameservers (ns-ovh.sslip.io and so on).
|
||||
|
||||
### Using the sslip.io domain
|
||||
|
||||
You can procure a [wildcard](https://en.wikipedia.org/wiki/Wildcard_certificate)
|
||||
certificate (e.g. `*.52-0-56-137.sslip.io`) from a certificate authority (e.g.
|
||||
Let's Encrypt) using the [DNS-01
|
||||
challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge).
|
||||
|
||||
You'll need the following:
|
||||
|
||||
- An internet-accessible DNS server that's authoritative for its `sslip.io`
|
||||
subdomain For example, if the DNS server's IP address is `52.187.42.158`, the
|
||||
DNS server would need to be authoritative for the domain
|
||||
`52-187-42-158.sslip.io`. Pro-tip: it only needs to be authoritative for the
|
||||
`_acme-challenge` subdomain, e.g. `_acme-challenge.52-187-42-158.sslip.io`;
|
||||
furthermore, it only needs to return TXT records.
|
||||
|
||||
How to test that your DNS server is working properly (assuming you've set a
|
||||
TXT record, "I love my dog"):
|
||||
|
||||
```
|
||||
dig _acme-challenge.52-187-42-158.sslip.io txt
|
||||
...
|
||||
_acme-challenge.52-187-42-158.sslip.io 604800 IN TXT "I love my dog"
|
||||
...
|
||||
```
|
||||
|
||||
- An [ACME
|
||||
v2](https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment)
|
||||
protocol client; I use [acme.sh](https://github.com/acmesh-official/acme.sh).
|
||||
The ACME client must be able to update the TXT records of your DNS server.
|
||||
|
||||
### Using the Wildcard Certificate
|
||||
|
||||
Once you've procured the wildcard certificate, you can install it on your
|
||||
internal webservers for URLS of the following format:
|
||||
https://*internal-ip.external-ip*.sslip.io (e.g.
|
||||
<https://www-192-168-0-10.52-187-42-158.sslip.io>). Note that the _internal-ip_
|
||||
portion of the URL _must_ be dash-separated, not dot-separated, for the wildcard
|
||||
certificate to work properly.
|
||||
|
||||
Tech note: wildcard certificates can be used for development for machines behind
|
||||
a firewall using non-routable IP addresses (10/8, 172.16/12, 192.168/16) by
|
||||
taking advantage of the manner which `sslip.io` parses hostnames with embedded
|
||||
IP addresses: left-to-right. The internal IP address is parsed first and
|
||||
returned as the IP address of the hostname.
|
||||
|
||||
### How Do I Set Up an External DNS Server?
|
||||
|
||||
The external IP might be from your local network (forward port 53 at your
|
||||
router), or from a cloud provider (GCP, AWS, etc.). It might even be from a
|
||||
public DNS service (e.g. [Cloudflare](https://www.cloudflare.com/), [AWS Route
|
||||
53](https://aws.amazon.com/route53/), my perennial favorite
|
||||
[easyDNS](https://easydns.com/), etc.). If not using a public DNS service, you
|
||||
need to run your own DNS server (e.g.
|
||||
[acme-dns](https://github.com/joohoi/acme-dns), the venerable
|
||||
[BIND](https://en.wikipedia.org/wiki/BIND), the opinionated
|
||||
[djbdns](https://cr.yp.to/djbdns.html), or my personal
|
||||
[wildcard-dns-http-server](https://github.com/cunnie/sslip.io/tree/main/src/wildcard-dns-http-server),
|
||||
etc.). You can use any ACME client
|
||||
([acme.sh](https://github.com/acmesh-official/acme.sh),
|
||||
[Certbot](https://certbot.eff.org/), etc.), but you must configure it to request
|
||||
a wildcard certificate for \*._external-ip_.sslip.io, which requires configuring
|
||||
the DNS-01 challenge to use DNS server chosen.
|
||||
|
||||
#### Example
|
||||
|
||||
In the following example, we create a webserver on Google Cloud Platform (GCP)
|
||||
to acquire a wildcard certificate. We use the ACME client acme.sh and the
|
||||
DNS server wildcard-dns-http-server:
|
||||
|
||||
```bash
|
||||
gcloud auth login
|
||||
# set your project; mine is "blabbertabber"
|
||||
gcloud config set project blabbertabber
|
||||
# create your VM
|
||||
gcloud compute instances create \
|
||||
--image-project "ubuntu-os-cloud" \
|
||||
--image-family "ubuntu-2004-lts" \
|
||||
--machine-type f1-micro \
|
||||
--boot-disk-size 40 \
|
||||
--boot-disk-type pd-ssd \
|
||||
--zone "us-west1-a" \
|
||||
sslip
|
||||
# get the IP, e.g. 35.199.174.9
|
||||
export NAT_IP=$(gcloud compute instances list --filter="name=('sslip')" --format=json | \
|
||||
jq -r '.[0].networkInterfaces[0].accessConfigs[0].natIP')
|
||||
echo $NAT_IP
|
||||
# get the fully-qualified domain name, e.g. 35-199-174-9.sslip.io
|
||||
export FQDN=${NAT_IP//./-}.sslip.io
|
||||
echo $FQDN
|
||||
# set IP & FQDN on the VM because we'll need them later
|
||||
gcloud compute ssh --command="echo export FQDN=$FQDN IP=$IP >> ~/.bashrc" --zone=us-west1-a sslip
|
||||
# create the rules to allow DNS (and ICMP/ping) inbound
|
||||
gcloud compute firewall-rules create sslip-io-allow-dns \
|
||||
--allow udp:53,icmp \
|
||||
--network=default \
|
||||
--source-ranges 0.0.0.0/0 \
|
||||
# ssh onto the VM
|
||||
gcloud compute ssh sslip -- -A
|
||||
# install docker
|
||||
sudo apt update && sudo apt upgrade -y && sudo apt install -y docker.io jq
|
||||
# add us to the docker group
|
||||
sudo addgroup $USER docker
|
||||
newgrp docker
|
||||
# Create the necessary directories
|
||||
mkdir -p tls/
|
||||
# disable systemd-resolved to fix "Error starting userland proxy: listen tcp 0.0.0.0:53: bind: address already in use."
|
||||
# thanks https://askubuntu.com/questions/907246/how-to-disable-systemd-resolved-in-ubuntu
|
||||
sudo systemctl disable systemd-resolved
|
||||
sudo systemctl stop systemd-resolved
|
||||
echo nameserver 8.8.8.8 | sudo tee /etc/resolv.conf
|
||||
# Let's start it up:
|
||||
docker run -it --rm --name wildcard \
|
||||
-p 53:53/udp \
|
||||
-p 80:80 \
|
||||
cunnie/wildcard-dns-http-server &
|
||||
dig +short TXT does.not.matter.example.com @localhost
|
||||
# You should see `"Set this TXT record ..."`
|
||||
export ACMEDNS_UPDATE_URL="http://localhost/update"
|
||||
docker run --rm -it \
|
||||
-v $PWD/tls:/acme.sh \
|
||||
-e ACMEDNS_UPDATE_URL \
|
||||
--net=host \
|
||||
neilpang/acme.sh \
|
||||
--issue \
|
||||
--debug \
|
||||
-d $FQDN \
|
||||
-d *.$FQDN \
|
||||
--dns dns_acmedns
|
||||
ls tls/$FQDN # you'll see the new cert, key, certificate
|
||||
openssl x509 -in tls/$FQDN/$FQDN.cer -noout -text # read the cert info
|
||||
```
|
||||
|
||||
Save the cert, key, certificate, intermediate ca, fullchain cert. They are in
|
||||
`tls/$FQDN/`.
|
||||
|
||||
Clean-up:
|
||||
|
||||
```
|
||||
gcloud compute firewall-rules delete sslip-io-allow-dns
|
||||
gcloud compute instances delete sslip
|
||||
```
|
||||
|
||||
#### Troubleshooting / Debugging
|
||||
|
||||
Run the server in one window so you can see the output, and then ssh into
|
||||
another window and watch the log output in realtime.
|
||||
|
||||
```
|
||||
gcloud compute ssh sslip -- -A
|
||||
docker run -it --rm --name wildcard \
|
||||
-p 53:53/udp \
|
||||
-p 80:80 \
|
||||
cunnie/wildcard-dns-http-server
|
||||
```
|
||||
|
||||
Notes about the logging output: any line that has the string "`TypeTXT →`" is
|
||||
output from the DNS server; everything else is output from the HTTP server which
|
||||
is used to create TXT records which the DNS server serves.
|
||||
|
||||
Use `acme.sh`'s `--staging` flag to make sure it works (so you don't run into
|
||||
Let's Encrypt's [rate limits](https://letsencrypt.org/docs/rate-limits/) with
|
||||
failed attempts).
|
||||
|
||||
```
|
||||
docker run --rm -it \
|
||||
-v $PWD/tls:/acme.sh \
|
||||
-e ACMEDNS_UPDATE_URL \
|
||||
--net=host \
|
||||
neilpang/acme.sh \
|
||||
--issue \
|
||||
--staging \
|
||||
--debug \
|
||||
-d *.$FQDN \
|
||||
--dns dns_acmedns
|
||||
```
|
@@ -1,51 +0,0 @@
|
||||
# cunnie/wildcard-dns-http-server: sslip.io wildcard DNS/HTTP server Dockerfile
|
||||
|
||||
# This DNS/HTTP server enables the procurement of wildcard certs for sslip.io
|
||||
# subdomains. It's meant to be run on the server whose IP address is the
|
||||
# subdomain. e.g. if the subdomain was '207-44-147-10.sslip.io', then this
|
||||
# should be run on the server whose IP address is 207.44.147.10, and this will
|
||||
# procure a wildcard cert for *.207-44-147-10.sslip.io
|
||||
|
||||
# This won't work for private addresses such as 10.0.1.10 or 192.168.0.1.
|
||||
|
||||
# Dockerfile of a (Golang-based) DNS/HTTP server.
|
||||
|
||||
# - the DNS server only responds to TXT queries, and always responds to TXT queries,
|
||||
# and always responds with the same TXT record
|
||||
# - the HTTP server allows you to update the TXT record by POST'ing to the /update
|
||||
# endpoint with a JSON body of `{"txt":"the-new-TXT-record"}`. The endpoint
|
||||
# is compatible with acme-dns.
|
||||
# - acme.sh can be configured to update the DNS TXT record via HTTPS.
|
||||
|
||||
# To build:
|
||||
|
||||
# DOCKER_BUILD_DIR=$PWD
|
||||
# pushd ../src/wildcard-dns-http-server/
|
||||
# GOOS=linux GOARCH=amd64 go build -o $DOCKER_BUILD_DIR/wildcard-dns-http-server
|
||||
# popd
|
||||
# docker build . -f Dockerfile-wildcard-dns-http-server -t cunnie/wildcard-dns-http-server
|
||||
|
||||
# Typical start command:
|
||||
|
||||
# docker run -it --rm -p 53:53/udp -p 80:80 cunnie/wildcard-dns-http-server
|
||||
|
||||
# To test from host:
|
||||
|
||||
# dig +short txt 127-0-0-1.example.com @localhost
|
||||
# "Set this TXT record: curl -X POST http://localhost/update -d '{\"txt\":\"Certificate Authority's validation token\"}'"
|
||||
# curl -X POST http://localhost/update -d '{"txt":"new-TXT-record"}'
|
||||
# dig +short txt any-domain-you-want @localhost
|
||||
# "new-TXT-record"
|
||||
|
||||
FROM alpine AS sslip.io
|
||||
|
||||
LABEL org.opencontainers.image.authors="Brian Cunnie <brian.cunnie@gmail.com>"
|
||||
|
||||
COPY wildcard-dns-http-server /usr/sbin/wildcard-dns-http-server
|
||||
|
||||
ENTRYPOINT ["/usr/sbin/wildcard-dns-http-server"]
|
||||
|
||||
# DNS listens on port 53 UDP
|
||||
# The `EXPOSE` directive doesn't do much in our case. We use it for documentation.
|
||||
EXPOSE 53/udp
|
||||
EXPOSE 80/tcp
|
@@ -172,9 +172,6 @@ dig @localhost 127-0-0-1.nip.io +short # returns "127.0.0.1"</pre>
|
||||
https://www.52.0.56.137.xip.example.com/.
|
||||
</p>
|
||||
</div>
|
||||
<p>if you're interested in acquiring a wildcard certificate for your nip.io domain, e.g.
|
||||
"*.52-0-56-137.nip.io", the procedure is described <a
|
||||
href="https://github.com/cunnie/sslip.io/blob/main/docs/wildcard.md">here</a>.</p>
|
||||
<h3 id="experimental">Experimental Features</h3>
|
||||
<p>Experimental features can change; don't depend on them.</p>
|
||||
<h4 id="whatismyip">Determining Your External IP Address via DNS Lookup</h4>
|
||||
|
1
src/.gitignore
vendored
1
src/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.idea/
|
@@ -1,5 +0,0 @@
|
||||
module github.com/cunnie/sslip.io/src/wildcard-dns-http-server
|
||||
|
||||
go 1.15
|
||||
|
||||
require golang.org/x/net v0.7.0
|
@@ -1,28 +0,0 @@
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
@@ -1,236 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
var txts = []string{`Set this TXT record: curl -X POST http://localhost/update -d '{"txt":"Certificate Authority validation token"}'`}
|
||||
|
||||
// Txt is for parsing the JSON POST to set the DNS TXT record
|
||||
type Txt struct {
|
||||
Txt string `json:"txt"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
var wg sync.WaitGroup
|
||||
log.Println("DNS: starting up.")
|
||||
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 53})
|
||||
switch {
|
||||
case err == nil:
|
||||
log.Println(`DNS: Successfully bound to all interfaces, port 53.`)
|
||||
wg.Add(1)
|
||||
go dnsServer(conn, &wg)
|
||||
case isErrorPermissionsError(err):
|
||||
log.Println("DNS: Try invoking me with `sudo` because I don't have permission to bind to port 53.")
|
||||
log.Fatal("DNS: " + err.Error())
|
||||
case isErrorAddressAlreadyInUse(err):
|
||||
log.Println(`DNS: I couldn't bind to "0.0.0.0:53" (INADDR_ANY, all interfaces), so I'll try to bind to each address individually.`)
|
||||
ipCIDRs := listLocalIPCIDRs()
|
||||
var boundIPsPorts, unboundIPs []string
|
||||
for _, ipCIDR := range ipCIDRs {
|
||||
ip, _, err := net.ParseCIDR(ipCIDR)
|
||||
if err != nil {
|
||||
log.Printf(`DNS: I couldn't parse the local interface "%s".`, ipCIDR)
|
||||
continue
|
||||
}
|
||||
conn, err = net.ListenUDP("udp", &net.UDPAddr{
|
||||
IP: ip,
|
||||
Port: 53,
|
||||
Zone: "",
|
||||
})
|
||||
if err != nil {
|
||||
unboundIPs = append(unboundIPs, ip.String())
|
||||
} else {
|
||||
wg.Add(1)
|
||||
boundIPsPorts = append(boundIPsPorts, conn.LocalAddr().String())
|
||||
go dnsServer(conn, &wg)
|
||||
}
|
||||
}
|
||||
if len(boundIPsPorts) > 0 {
|
||||
log.Printf(`DNS: I bound to the following: "%s"`, strings.Join(boundIPsPorts, `", "`))
|
||||
}
|
||||
if len(unboundIPs) > 0 {
|
||||
log.Printf(`DNS: I couldn't bind to the following IPs: "%s"`, strings.Join(unboundIPs, `", "`))
|
||||
}
|
||||
default:
|
||||
log.Fatal("DNS: " + err.Error())
|
||||
}
|
||||
wg.Add(1)
|
||||
go httpServer(&wg)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func dnsServer(conn *net.UDPConn, group *sync.WaitGroup) {
|
||||
var query dnsmessage.Message
|
||||
|
||||
defer group.Done()
|
||||
queryRaw := make([]byte, 512)
|
||||
for {
|
||||
_, addr, err := conn.ReadFromUDP(queryRaw)
|
||||
if err != nil {
|
||||
log.Println("DNS: " + err.Error())
|
||||
continue
|
||||
}
|
||||
err = query.Unpack(queryRaw)
|
||||
if err != nil {
|
||||
log.Println("DNS: " + err.Error())
|
||||
continue
|
||||
}
|
||||
// Technically, there can be multiple questions in a DNS message; practically, there's only one
|
||||
if len(query.Questions) != 1 {
|
||||
log.Printf("DNS: I expected one question but got %d.\n", len(query.Questions))
|
||||
continue
|
||||
}
|
||||
// We only return answers to TXT queries, nothing else
|
||||
if query.Questions[0].Type != dnsmessage.TypeTXT {
|
||||
log.Println("DNS: I expected a question for a TypeTXT record but got a question for a " + query.Questions[0].Type.String() + " record.")
|
||||
continue
|
||||
}
|
||||
var txtAnswers = []dnsmessage.Resource{}
|
||||
for _, txt := range txts {
|
||||
txtAnswers = append(txtAnswers, dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{
|
||||
Name: query.Questions[0].Name,
|
||||
Type: dnsmessage.TypeTXT,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: 60,
|
||||
},
|
||||
Body: &dnsmessage.TXTResource{TXT: []string{txt}},
|
||||
})
|
||||
}
|
||||
reply := dnsmessage.Message{
|
||||
Header: dnsmessage.Header{
|
||||
ID: query.ID,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
RecursionDesired: query.RecursionDesired,
|
||||
},
|
||||
Questions: query.Questions,
|
||||
Answers: txtAnswers,
|
||||
}
|
||||
replyRaw, err := reply.Pack()
|
||||
if err != nil {
|
||||
log.Println("DNS: " + err.Error())
|
||||
continue
|
||||
}
|
||||
_, err = conn.WriteToUDP(replyRaw, addr)
|
||||
if err != nil {
|
||||
log.Println("DNS: " + err.Error())
|
||||
continue
|
||||
}
|
||||
log.Printf("DNS: %v.%d %s → \"%v\"\n", addr.IP, addr.Port, query.Questions[0].Type.String(), txts)
|
||||
}
|
||||
}
|
||||
|
||||
func httpServer(group *sync.WaitGroup) {
|
||||
defer group.Done()
|
||||
log.Println("HTTP: starting up.")
|
||||
http.HandleFunc("/", usageHandler)
|
||||
http.HandleFunc("/update", updateTxtHandler)
|
||||
log.Fatal("HTTP: " + http.ListenAndServe(":80", nil).Error())
|
||||
}
|
||||
|
||||
func usageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := fmt.Fprintln(w, `Set the TXT record: curl -X POST http://localhost/update -d '{"txt":"Certificate Authority's validation token"}'`)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Println("HTTP: " + err.Error())
|
||||
}
|
||||
log.Printf("HTTP: wrong path (%s) with method (%s).\n", r.URL.Path, r.Method)
|
||||
}
|
||||
|
||||
func updateTxtHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var err error
|
||||
if r.Method != http.MethodPost {
|
||||
err = errors.New("/update requires POST method, not " + r.Method + " method")
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
log.Println("HTTP: " + err.Error())
|
||||
return
|
||||
}
|
||||
var body []byte
|
||||
if body, err = ioutil.ReadAll(r.Body); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Println("HTTP: " + err.Error())
|
||||
return
|
||||
}
|
||||
var updateTxt Txt
|
||||
if err := json.Unmarshal(body, &updateTxt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Println("HTTP: " + err.Error())
|
||||
return
|
||||
}
|
||||
if body, err = json.Marshal(updateTxt); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Println("HTTP: " + err.Error())
|
||||
return
|
||||
}
|
||||
if _, err = fmt.Fprintf(w, string(body)); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
log.Println("HTTP: " + err.Error())
|
||||
return
|
||||
}
|
||||
log.Println("HTTP: Creating new TXT record \"" + updateTxt.Txt + "\".")
|
||||
// this is the money shot, where we create a new DNS TXT record to what was in the POST request
|
||||
txts = append(txts, updateTxt.Txt)
|
||||
}
|
||||
func listLocalIPCIDRs() []string {
|
||||
var ifaces []net.Interface
|
||||
var cidrStrings []string
|
||||
var err error
|
||||
if ifaces, err = net.Interfaces(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
var cidrs []net.Addr
|
||||
if cidrs, err = iface.Addrs(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
for _, cidr := range cidrs {
|
||||
cidrStrings = append(cidrStrings, cidr.String())
|
||||
}
|
||||
}
|
||||
return cidrStrings
|
||||
}
|
||||
|
||||
// Thanks https://stackoverflow.com/a/52152912/2510873
|
||||
func isErrorAddressAlreadyInUse(err error) bool {
|
||||
var eOsSyscall *os.SyscallError
|
||||
if !errors.As(err, &eOsSyscall) {
|
||||
return false
|
||||
}
|
||||
var errErrno syscall.Errno // doesn't need a "*" (ptr) because it's already a ptr (uintptr)
|
||||
if !errors.As(eOsSyscall, &errErrno) {
|
||||
return false
|
||||
}
|
||||
if errErrno == syscall.EADDRINUSE {
|
||||
return true
|
||||
}
|
||||
const WSAEADDRINUSE = 10048
|
||||
if runtime.GOOS == "windows" && errErrno == WSAEADDRINUSE {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isErrorPermissionsError(err error) bool {
|
||||
var eOsSyscall *os.SyscallError
|
||||
if errors.As(err, &eOsSyscall) {
|
||||
if os.IsPermission(eOsSyscall) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
Reference in New Issue
Block a user