wildcard-dns-http-server: better error-checking

- when DNS gets a permission error, it helpfully suggests using `sudo`
- when DNS can't bind to `INADDR_ANY`, it's probably because it's Fedora
running `systemd.resolved` on port 53 of 127.0.0.53, so we try to bind
to each address individually.
- we don't implement similar checks for the HTTP server:
  - if it's a permission problem, the DNS server has already warned the
  user.
  - if it's a binding problem, the user is probably running an HTTP
  server bound to `INADDR_ANY`, so we might as well exit.
- we ported this code from main `sslip.io` DNS server.
This commit is contained in:
Brian Cunnie
2021-02-09 06:49:00 -08:00
parent 7ad3f4a22f
commit 3fc089b7a7

View File

@@ -8,7 +8,11 @@ import (
"log"
"net"
"net/http"
"os"
"runtime"
"strings"
"sync"
"syscall"
"golang.org/x/net/dns/dnsmessage"
)
@@ -21,24 +25,58 @@ type Txt struct {
}
func main() {
var wg sync.WaitGroup
log.Println("DNS: starting up.")
conn, err := net.ListenUDP("udp", &net.UDPAddr{Port: 53})
if err != nil {
log.Fatal(err.Error())
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())
}
var group sync.WaitGroup
group.Add(1)
go dnsServer(conn, &group)
group.Add(1)
go httpServer(&group)
group.Wait()
wg.Add(1)
go httpServer(&wg)
wg.Wait()
}
func dnsServer(conn *net.UDPConn, group *sync.WaitGroup) {
var query dnsmessage.Message
defer group.Done()
log.Println("DNS: starting up.")
queryRaw := make([]byte, 512)
for {
_, addr, err := conn.ReadFromUDP(queryRaw)
@@ -111,6 +149,7 @@ func usageHandler(w http.ResponseWriter, r *http.Request) {
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) {
@@ -143,7 +182,55 @@ func updateTxtHandler(w http.ResponseWriter, r *http.Request) {
log.Println("HTTP: " + err.Error())
return
}
log.Println("Creating new TXT record \"" + updateTxt.Txt + "\".")
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
}