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:
Brian Cunnie
2025-08-12 19:38:50 -07:00
parent 726bf8707c
commit 16944e6adf
8 changed files with 1 additions and 521 deletions

View File

@@ -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 `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 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 set address records for the new name servers with the `-addresses` flag (see
below). Exception: `_acme-challenge` records are handled differently to below).
accommodate the procurement of Let's Encrypt wildcard certificates; you can
read more about that procedure [here](docs/wildcard.md)
- `-addresses` overrides the default A/AAAA (IPv4/IPv6) address records. For - `-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 example, here's how we set the IPv4 record & IPv6 record for our nameserver
(in the `-nameservers` example above), ns1.example.com: `-addresses (in the `-nameservers` example above), ns1.example.com: `-addresses

View File

@@ -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
```

View File

@@ -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

View File

@@ -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/. https://www.52.0.56.137.xip.example.com/.
</p> </p>
</div> </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> <h3 id="experimental">Experimental Features</h3>
<p>Experimental features can change; don't depend on them.</p> <p>Experimental features can change; don't depend on them.</p>
<h4 id="whatismyip">Determining Your External IP Address via DNS Lookup</h4> <h4 id="whatismyip">Determining Your External IP Address via DNS Lookup</h4>

1
src/.gitignore vendored
View File

@@ -1 +0,0 @@
.idea/

View File

@@ -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

View File

@@ -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=

View File

@@ -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
}