diff --git a/Dockerfile b/Dockerfile index 3fe614c..653c1a9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,15 +8,20 @@ RUN CGO_ENABLED=0 go build -o /app/local-ip FROM gcr.io/distroless/base-debian12:latest -ENV PORT 53 +WORKDIR /local-ip -WORKDIR /app +COPY --from=build /app/local-ip /local-ip/local-ip +COPY --from=build /app/http/static /local-ip/http/static -COPY --from=build /app/local-ip /app/local-ip -COPY --from=build /app/http/static /app/http/static -COPY ./.lego /app/.lego +VOLUME /local-ip/.lego + +# DNS +EXPOSE 53/udp +# HTTP +EXPOSE 80/tcp +# HTTPS +EXPOSE 443/tcp -EXPOSE $PORT USER root -CMD ["/app/local-ip"] +CMD ["/local-ip/local-ip"] diff --git a/README.md b/README.md index fd65450..6c12e98 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # local-ip.sh -[local-ip.sh](https://local-ip.sh) is a magic domain name that provides wildcard DNS for any IP address. +[local-ip.sh](https://local-ip.sh) is a magic domain name that provides wildcard DNS for any IP address. It is heavily inspired by [local-ip.co](http://local-ip.co), [sslip.io](https://sslip.io), and [xip.io](https://xip.io) ## Usage @@ -22,16 +22,13 @@ dig @localhost 127.0.0.1.my.local-ip.sh +short local-ip.sh packs up: - an authoritative DNS server that answers queries for the zone `local-ip.sh` - - a Let's Encrypt client that takes care of obtaining and renewing the wildcard certificate for `*.local-ip.sh` using the DNS-01 challenge - - an HTTP server that serves the certificate files + - a Let's Encrypt client that takes care of obtaining and renewing the wildcard certificate for `*.local-ip.sh` and the root certificate for `local-ip.sh` using the [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) + - an HTTP server that serves static files, including the certificate files -It answers queries with the IPv4 address it may find in the subdomain by pattern matching the FQDN. +It answers queries with the IPv4 address it may find in the subdomain by pattern matching the FQDN. It registers an account to Let's Encrypt's ACME server to obtain the wildcard certificate on the first run and then renew -it about a month before it expires. The account file and the associated key used to request a certificate under the `.lego` -directory but the certificate's files are stored in `/certs` at the root of the filesystem. I've done it this way to mount -a persistent storage volume there and keep the files between deployments without tracking them in git but feel free to -change this behavior in [`certs/certs.go`](./certs/certs.go) and in [`http/server.go`](./http/server.go) -if you're planning to self-host it. +it about a month before it expires. The account file and the associated key used to request a certificate under the `./.lego/accounts` +directory and the certificate's files are stored in `./.lego/certs`. The certificate files are served by an HTTP server on the arbitrary port `:9229` that is intentionally not exposed to the internet. [The website](https://local-ip.sh) is connected to the same private network as the service and serves diff --git a/certs/certs.go b/certs/certs.go index b1ccd90..960483f 100644 --- a/certs/certs.go +++ b/certs/certs.go @@ -92,19 +92,19 @@ func (c *certsClient) renewCertificates() { } func persistFiles(certificates *certificate.Resource, certType string) { - err := os.MkdirAll(fmt.Sprintf("/certs/%s", certType), 0o755) + err := os.MkdirAll(fmt.Sprintf("./.lego/certs/%s", certType), 0o755) if err != nil { - utils.Logger.Fatal().Err(err).Msgf("Failed to mkdir /certs/%s", certType) + utils.Logger.Fatal().Err(err).Msgf("Failed to mkdir ./.lego/certs/%s", certType) } - err = os.WriteFile(fmt.Sprintf("/certs/%s/server.pem", certType), certificates.Certificate, 0o644) + err = os.WriteFile(fmt.Sprintf("./.lego/certs/%s/server.pem", certType), certificates.Certificate, 0o644) if err != nil { - utils.Logger.Fatal().Err(err).Msgf("Failed to save /certs/%s/server.pem", certType) + utils.Logger.Fatal().Err(err).Msgf("Failed to save ./.lego/certs/%s/server.pem", certType) } - os.WriteFile(fmt.Sprintf("/certs/%s/server.key", certType), certificates.PrivateKey, 0o644) + os.WriteFile(fmt.Sprintf("./.lego/certs/%s/server.key", certType), certificates.PrivateKey, 0o644) if err != nil { - utils.Logger.Fatal().Err(err).Msgf("Failed to save /certs/%s/server.key", certType) + utils.Logger.Fatal().Err(err).Msgf("Failed to save ./.lego/certs/%s/server.key", certType) } jsonBytes, err := json.MarshalIndent(certificates, "", "\t") @@ -112,9 +112,9 @@ func persistFiles(certificates *certificate.Resource, certType string) { utils.Logger.Fatal().Err(err).Msg("Failed to marshal certificates to JSON") } - err = os.WriteFile(fmt.Sprintf("/certs/%s/output.json", certType), jsonBytes, 0o644) + err = os.WriteFile(fmt.Sprintf("./.lego/certs/%s/output.json", certType), jsonBytes, 0o644) if err != nil { - utils.Logger.Fatal().Err(err).Msgf("Failed to save /certs/%s/output.json", certType) + utils.Logger.Fatal().Err(err).Msgf("Failed to save ./.lego/certs/%s/output.json", certType) } } @@ -140,7 +140,7 @@ func NewCertsClient(xip *xip.Xip, user *Account) *certsClient { } func getLastCertificate(legoClient *lego.Client, certType string) *certificate.Resource { - jsonBytes, err := os.ReadFile(fmt.Sprintf("/certs/%s/output.json", certType)) + jsonBytes, err := os.ReadFile(fmt.Sprintf("./.lego/certs/%s/output.json", certType)) if err != nil { if strings.Contains(err.Error(), "no such file or directory") { return nil diff --git a/fly.toml b/fly.toml index 3aeb3da..5b5bd1d 100644 --- a/fly.toml +++ b/fly.toml @@ -17,8 +17,8 @@ auto_rollback = true PORT = "53" [mounts] -source = "certs" -destination = "/certs" +source = "lego" +destination = "/local-ip/.lego" [[services]] protocol = "udp" diff --git a/http/server.go b/http/server.go index a4f49d0..f69dcc3 100644 --- a/http/server.go +++ b/http/server.go @@ -13,11 +13,11 @@ import ( func registerHandlers() { http.HandleFunc("GET /server.key", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/octet-stream") - http.ServeFile(w, r, "/certs/wildcard/server.key") + http.ServeFile(w, r, "./.lego/certs/wildcard/server.key") }) http.HandleFunc("GET /server.pem", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/x-x509-ca-cert") - http.ServeFile(w, r, "/certs/wildcard/server.pem") + http.ServeFile(w, r, "./.lego/certs/wildcard/server.pem") }) http.HandleFunc("GET /og.png", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "image/png; charset=utf-8") @@ -58,13 +58,13 @@ func serveHttp() *http.Server { func waitForCertificate(ready chan bool) { for { - _, err := os.Stat("/certs/root/output.json") + _, err := os.Stat("./.lego/certs/root/output.json") if err != nil { if strings.Contains(err.Error(), "no such file or directory") { time.Sleep(1 * time.Second) continue } - utils.Logger.Fatal().Err(err).Msg("Unexpected error while looking for /certs/root/output.json") + utils.Logger.Fatal().Err(err).Msg("Unexpected error while looking for ./.lego/certs/root/output.json") } break } @@ -100,7 +100,7 @@ func redirectHttpToHttps() { func serveHttps() { utils.Logger.Info().Msg("Starting up HTTPS server on :443") httpsServer := &http.Server{Addr: ":https"} - go httpsServer.ListenAndServeTLS("/certs/root/server.pem", "/certs/root/server.key") + go httpsServer.ListenAndServeTLS("./.lego/certs/root/server.pem", "./.lego/certs/root/server.key") } func ServeHttp() {