diff --git a/Dockerfile b/Dockerfile
index 3d6c462..391942a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,12 +10,13 @@ FROM gcr.io/distroless/base-debian12:latest
ENV PORT 53
-WORKDIR /
+WORKDIR /app
-COPY --from=build /app/local-ip /
-COPY ./.lego /.lego
+COPY --from=build /app/local-ip /app/local-ip
+COPY --from=build /app/http/static /app/http/static
+COPY ./.lego /app/.lego
EXPOSE $PORT
USER root
-CMD ["/local-ip"]
+CMD ["/app/local-ip"]
diff --git a/README.md b/README.md
index 7e23a0a..fd65450 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ as a proxy to access the files securely.
## Self-hosting
-I'm currently hosting [local-ip.sh](https://www.local-ip.sh) at [Fly.io](https://fly.io) but you can host the service yourself
+I'm currently hosting [local-ip.sh](https://local-ip.sh) at [Fly.io](https://fly.io) but you can host the service yourself
if you're into that kind of thing. Note that you will need to edit your domain's glue records so make sure your registrar allows it.
You will essentially need to:
diff --git a/certs/certs.go b/certs/certs.go
index 4aeca48..73e87eb 100644
--- a/certs/certs.go
+++ b/certs/certs.go
@@ -2,6 +2,7 @@ package certs
import (
"encoding/json"
+ "fmt"
"log"
"os"
"strings"
@@ -15,14 +16,32 @@ import (
)
type certsClient struct {
- legoClient *lego.Client
- lastCertificate *certificate.Resource
+ legoClient *lego.Client
+ lastWildcardCertificate *certificate.Resource
+ lastRootCertificate *certificate.Resource
}
-func (c *certsClient) RequestCertificate() {
- log.Println("requesting a certificate")
- if c.lastCertificate != nil {
- certificates, err := certcrypto.ParsePEMBundle(c.lastCertificate.Certificate)
+func (c *certsClient) RequestCertificates() {
+ c.requestCertificate("wildcard")
+ c.requestCertificate("root")
+}
+
+func (c *certsClient) requestCertificate(certType string) {
+ var lastCertificate *certificate.Resource
+ var domains []string
+ if certType == "wildcard" {
+ lastCertificate = c.lastWildcardCertificate
+ domains = []string{"*.local-ip.sh"}
+ } else if certType == "root" {
+ lastCertificate = c.lastRootCertificate
+ domains = []string{"local-ip.sh"}
+ } else {
+ log.Fatalf("Unexpected certType %s. Only \"wildcard\" and \"root\" are supported", certType)
+ }
+
+ log.Printf("requesting %s certificate\n", certType)
+ if lastCertificate != nil {
+ certificates, err := certcrypto.ParsePEMBundle(c.lastWildcardCertificate.Certificate)
if err != nil {
log.Fatal(err)
}
@@ -34,45 +53,55 @@ func (c *certsClient) RequestCertificate() {
return
}
- c.renewCertificate()
+ c.renewCertificates()
return
}
certificates, err := c.legoClient.Certificate.Obtain(certificate.ObtainRequest{
- Domains: []string{"*.local-ip.sh"},
+ Domains: domains,
Bundle: true,
})
if err != nil {
log.Fatal(err)
}
- c.lastCertificate = certificates
+ if certType == "wildcard" {
+ c.lastWildcardCertificate = certificates
+ } else if certType == "root" {
+ c.lastRootCertificate = certificates
+ }
+
+ persistFiles(certificates, certType)
- persistFiles(certificates)
- log.Printf("%#v\n", certificates)
}
-func (c *certsClient) renewCertificate() {
+func (c *certsClient) renewCertificates() {
log.Println("renewing currently existing certificate")
- certificates, err := c.legoClient.Certificate.Renew(*c.lastCertificate, true, false, "")
+ wildcardCertificate, err := c.legoClient.Certificate.Renew(*c.lastWildcardCertificate, true, false, "")
if err != nil {
log.Fatal(err)
}
+ c.lastWildcardCertificate = wildcardCertificate
+ persistFiles(wildcardCertificate, "wildcard")
- c.lastCertificate = certificates
+ rootCertificate, err := c.legoClient.Certificate.Renew(*c.lastRootCertificate, true, false, "")
+ if err != nil {
+ log.Fatal(err)
+ }
+ c.lastRootCertificate = rootCertificate
+ persistFiles(rootCertificate, "root")
- persistFiles(certificates)
- log.Printf("%#v\n", certificates)
}
-func persistFiles(certificates *certificate.Resource) {
- os.WriteFile("/certs/server.pem", certificates.Certificate, 0o644)
- os.WriteFile("/certs/server.key", certificates.PrivateKey, 0o644)
+func persistFiles(certificates *certificate.Resource, certType string) {
+ os.MkdirAll(fmt.Sprintf("/certs/%s", certType), 0o755)
+ os.WriteFile(fmt.Sprintf("/certs/%s/server.pem", certType), certificates.Certificate, 0o644)
+ os.WriteFile(fmt.Sprintf("/certs/%s/server.key", certType), certificates.PrivateKey, 0o644)
jsonBytes, err := json.MarshalIndent(certificates, "", "\t")
if err != nil {
log.Fatal(err)
}
- os.WriteFile("/certs/output.json", jsonBytes, 0o644)
+ os.WriteFile(fmt.Sprintf("/certs/%s/output.json", certType), jsonBytes, 0o644)
}
func NewCertsClient(xip *xip.Xip, user *Account) *certsClient {
@@ -86,16 +115,18 @@ func NewCertsClient(xip *xip.Xip, user *Account) *certsClient {
provider := newProviderLocalIp(xip)
legoClient.Challenge.SetDNS01Provider(provider, dns01.AddRecursiveNameservers([]string{"1.1.1.1:53", "8.8.8.8:53"}), dns01.DisableCompletePropagationRequirement())
- lastCertificate := getLastCertificate(legoClient)
+ lastWildcardCertificate := getLastCertificate(legoClient, "wildcard")
+ lastRootCertificate := getLastCertificate(legoClient, "root")
return &certsClient{
legoClient,
- lastCertificate,
+ lastWildcardCertificate,
+ lastRootCertificate,
}
}
-func getLastCertificate(legoClient *lego.Client) *certificate.Resource {
- jsonBytes, err := os.ReadFile("/certs/output.json")
+func getLastCertificate(legoClient *lego.Client, certType string) *certificate.Resource {
+ jsonBytes, err := os.ReadFile(fmt.Sprintf("/certs/%s/output.json", certType))
if err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return nil
diff --git a/certs/config.go b/certs/config.go
index ab4033d..b9577b1 100644
--- a/certs/config.go
+++ b/certs/config.go
@@ -10,6 +10,7 @@ import (
const (
email = "admin@local-ip.sh"
caDirUrl = lego.LEDirectoryProduction
+ // caDirUrl = lego.LEDirectoryStaging
)
var (
diff --git a/fly.toml b/fly.toml
index bf8724d..3aeb3da 100644
--- a/fly.toml
+++ b/fly.toml
@@ -20,14 +20,6 @@ PORT = "53"
source = "certs"
destination = "/certs"
-[http_service]
-internal_port = 53
-force_https = true
-auto_stop_machines = false
-auto_start_machines = true
-min_machines_running = 1
-processes = ["app"]
-
[[services]]
protocol = "udp"
internal_port = 53
@@ -38,7 +30,33 @@ min_machines_running = 0
[[services.ports]]
port = 53
+[[services]]
+protocol = "tcp"
+internal_port = 80
+
+[[services.ports]]
+port = 80
+
+[[services]]
+protocol = "tcp"
+internal_port = 443
+
+[[services.ports]]
+port = 443
+
+# [[services.http_checks]]
+# interval = 10000
+# grace_period = "30s"
+# method = "get"
+# path = "/"
+# protocol = "https"
+# timeout = 15000
+# tls_skip_verify = false
+# tls_server_name = "local-ip.sh"
+# [services.http_checks.headers]
+
[[vm]]
+size = "shared-cpu-1x"
cpu_kind = "shared"
cpus = 1
memory_mb = 256
diff --git a/http/server.go b/http/server.go
index a2cdf9f..44bb673 100644
--- a/http/server.go
+++ b/http/server.go
@@ -3,17 +3,61 @@ package http
import (
"log"
"net/http"
+ "os"
+ "strings"
+ "time"
)
-func ServeCertificate() {
+func ServeHttp() {
+ waitForCertificate()
+
+ go http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ url := r.URL
+ url.Host = r.Host
+ url.Scheme = "https"
+ http.Redirect(w, r, url.String(), http.StatusMovedPermanently)
+ }))
+
http.HandleFunc("/server.key", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/octet-stream")
- http.ServeFile(w, r, "/certs/server.key")
+ http.ServeFile(w, r, "/certs/wildcard/server.key")
})
http.HandleFunc("/server.pem", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/x-x509-ca-cert")
- http.ServeFile(w, r, "/certs/server.pem")
+ http.ServeFile(w, r, "/certs/wildcard/server.pem")
})
- log.Printf("Serving cert files on :9229\n")
- http.ListenAndServe(":9229", nil)
+ http.HandleFunc("/og.png", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "image/png; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ http.ServeFile(w, r, "./http/static/og.png")
+ })
+ http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "image/x-icon; charset=utf-8")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ http.ServeFile(w, r, "./http/static/favicon.ico")
+ })
+ http.HandleFunc("/styles.css", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/css; charset=utf-8")
+ http.ServeFile(w, r, "./http/static/styles.css")
+ })
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ http.ServeFile(w, r, "./http/static/index.html")
+ })
+ log.Printf("Serving HTTPS server on :443\n")
+ http.ListenAndServeTLS(":443", "/certs/root/server.pem", "/certs/root/server.key", nil)
+}
+
+func waitForCertificate() {
+ for {
+ _, err := os.Stat("/certs/root/output.json")
+ if err != nil {
+ if strings.Contains(err.Error(), "no such file or directory") {
+ time.Sleep(1 * time.Second)
+ continue
+ }
+ log.Fatal(err)
+ }
+ break
+ }
}
diff --git a/http/static/favicon.ico b/http/static/favicon.ico
new file mode 100644
index 0000000..e29f8ee
Binary files /dev/null and b/http/static/favicon.ico differ
diff --git a/http/static/index.html b/http/static/index.html
new file mode 100644
index 0000000..d8af752
--- /dev/null
+++ b/http/static/index.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+ local-ip.sh
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ _ _ _ _
+ | | | | (_) | |
+ | | ___ ___ __ _| | _ _ __ ___| |__
+ | |/ _ \ / __/ _` | |_____| | '_ \ / __| '_ \
+ | | (_) | (_| (_| | |_____| | |_) |\__ \ | | |
+ |_|\___/ \___\__,_|_| |_| .__(_)___/_| |_|
+ | |
+ |_|
+
+
+
+
+
+
+ local-ip.sh is a magic domain name that provides wildcard DNS for any IP address. It is heavily
+ inspired by local-ip.co , sslip.io , and xip.io .
+ Quick example, say your LAN IP address is 192.168.1.10 . Using
+ local-ip.sh,
+ 192.168.1.10 .local-ip.sh resolves to 192.168.1.10
+ dots.192.168.1.10 .local-ip.sh resolves to 192.168.1.10
+ dashes.192-168-1-10 .local-ip.sh resolves to 192.168.1.10
+
+ ...and so on. You can use these domains to access virtual hosts on your development web server
+ from devices on your local network. No configuration required!
+ The best part is, you can serve your content over HTTPS with our TLS certificate for
+ *.local-ip.sh
:Be aware that wildcard certificates are not recursive, meaning they don't match
+ "sub-subdomains". In our case, this certificate will only match subdomains of
+ local-ip.sh
such as 192-168-1-10.local-ip.sh
where dashes separate
+ the numbers that make up the IP address.
+
+
+
+
+
+
+ local-ip.sh runs publicly a custom DNS server .
+ When your computer looks up a local-ip.sh domain, the local-ip.sh DNS server resolves to the IP address it extracts from the domain.
+
+
+ The TLS certificate is obtained from Let's Encrypt and renewed up to a month before it expires.
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/http/static/og.png b/http/static/og.png
new file mode 100644
index 0000000..d0c1781
Binary files /dev/null and b/http/static/og.png differ
diff --git a/http/static/styles.css b/http/static/styles.css
new file mode 100644
index 0000000..23e1261
--- /dev/null
+++ b/http/static/styles.css
@@ -0,0 +1,107 @@
+html {
+ background: #111;
+}
+
+body {
+ color: #728ea7;
+ display: flex;
+ flex-direction: column;
+ margin-inline: auto;
+ padding-left: 1.5em;
+ padding-right: 1.5em;
+ width: min(100%, 41.5rem);
+ margin-top: 50px;
+ margin-bottom: 50px;
+
+ font-family: ui-monospace, monospace;
+ font-size: 18px;
+ font-weight: bold;
+
+ -webkit-font-smoothing: antialiased;
+}
+
+header {
+ color: #7aa6da;
+ display: flex;
+}
+
+header > pre {
+ margin: 1rem auto;
+}
+
+main a {
+ color: #728ea7;
+}
+
+main a:hover {
+ background: #7aa6da;
+ color: #111;
+ text-decoration: none;
+}
+
+section:nth-child(n + 2) {
+ margin-top: 3rem;
+}
+
+section > main {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ row-gap: 2rem;
+}
+
+code {
+ padding: 0.2em 0.4em;
+ margin: 0;
+ font-size: 85%;
+ background-color: rgba(27, 31, 35, 0.95);
+ border-radius: 3px;
+ white-space: nowrap;
+}
+
+header span.dim {
+ color: #728ea7;
+}
+
+section strong {
+ color: #7aa6da;
+}
+
+footer {
+ margin: 5rem auto 0;
+ color: #556a7d;
+}
+
+footer a {
+ color: inherit;
+}
+
+footer a:hover {
+ color: #728ea7;
+}
+
+div.cursor {
+ display: inline-block;
+ background: #111;
+ margin-left: 1px;
+ margin-right: -1px;
+ animation: blink 2s linear 0s infinite;
+}
+
+@keyframes blink {
+ 0% {
+ background: #7aa6da;
+ }
+ 47% {
+ background: #728ea7;
+ }
+ 50% {
+ background: #111;
+ }
+ 97% {
+ background: #111;
+ }
+ 100% {
+ background: #728ea7;
+ }
+}
diff --git a/main.go b/main.go
index 449f308..9f4d601 100644
--- a/main.go
+++ b/main.go
@@ -20,16 +20,16 @@ func main() {
certsClient := certs.NewCertsClient(n, account)
time.Sleep(5 * time.Second)
- certsClient.RequestCertificate()
+ certsClient.RequestCertificates()
for {
// try to renew certificate every day
time.Sleep(24 * time.Hour)
- certsClient.RequestCertificate()
+ certsClient.RequestCertificates()
}
}()
- go http.ServeCertificate()
+ go http.ServeHttp()
n.StartServer()
}
diff --git a/xip/xip.go b/xip/xip.go
index c97969e..68c1aa3 100644
--- a/xip/xip.go
+++ b/xip/xip.go
@@ -23,6 +23,7 @@ type HardcodedRecord struct {
TXT *dns.TXT
MX []*dns.MX
CNAME []*dns.CNAME
+ SRV *dns.SRV
}
const (
@@ -54,47 +55,45 @@ var (
},
"local-ip.sh.": {
A: []*dns.A{
- {A: net.IPv4(66, 241, 125, 48)},
- },
- AAAA: []*dns.AAAA{
- {AAAA: net.IP{0x2a, 0x09, 0x82, 0x80, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0x1C, 0xC1, 0xC1}},
+ // {A: net.IPv4(66, 241, 125, 48)},
+ {A: net.IPv4(137, 66, 40, 11)}, // fly.io edge-only ip address
},
TXT: &dns.TXT{
Txt: []string{
"sl-verification=frudknyqpqlpgzbglkqnsmorfcvxrf",
- "v=spf1 include:simplelogin.co ~all",
+ "v=spf1 include:capsulecorp.dev ~all",
},
},
MX: []*dns.MX{
- {Preference: 10, Mx: "mx1.simplelogin.co."},
- {Preference: 20, Mx: "mx2.simplelogin.co."},
+ {Preference: 10, Mx: "email.capsulecorp.dev."},
+ },
+ },
+ "autodiscover.local-ip.sh.": {
+ CNAME: []*dns.CNAME{
+ {Target: "email.capsulecorp.dev"},
+ },
+ },
+ "_autodiscover._tcp.local-ip.sh.": {
+ SRV: &dns.SRV{
+ Target: "email.capsulecorp.dev 443",
+ },
+ },
+ "autoconfig.local-ip.sh.": {
+ CNAME: []*dns.CNAME{
+ {Target: "email.capsulecorp.dev"},
},
},
"_dmarc.local-ip.sh.": {
TXT: &dns.TXT{
- Txt: []string{"v=DMARC1; p=quarantine; pct=100; adkim=s; aspf=s"},
+ Txt: []string{"v=DMARC1; p=none; rua=mailto:postmaster@local-ip.sh; ruf=mailto:admin@local-ip.sh"},
},
},
"dkim._domainkey.local-ip.sh.": {
- CNAME: []*dns.CNAME{
- {Target: "dkim._domainkey.simplelogin.co."},
- },
- },
- "dkim02._domainkey.local-ip.sh.": {
- CNAME: []*dns.CNAME{
- {Target: "dkim02._domainkey.simplelogin.co."},
- },
- },
- "dkim03._domainkey.local-ip.sh.": {
- CNAME: []*dns.CNAME{
- {Target: "dkim03._domainkey.simplelogin.co."},
+ TXT: &dns.TXT{
+ Txt: []string{"v=DKIM1;k=rsa;t=s;s=email;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsMW6NFo34qzKRPbzK41GwbWncB8IDg1i2eA2VWznIVDmTzzsqILaBOGv2xokVpzZm0QRF9wSbeVUmvwEeQ7Z6wkfMjawenDEc3XxsNSvQUVBP6LU/xcm1zsR8wtD8r5J+Jm45pNFaateiM/kb/Eypp2ntdtd8CPsEgCEDpNb62LWdy0yzRdZ/M/fNn51UMN8hVFp4YfZngAt3bQwa6kPtgvTeqEbpNf5xanpDysNJt2S8zfqJMVGvnr8JaJiTv7ZlKMMp94aC5Ndcir1WbMyfmgSnGgemuCTVMWDGPJnXDi+8BQMH1b1hmTpWDiVdVlehyyWx5AfPrsWG9cEuDIfXwIDAQAB"},
},
},
"_acme-challenge.local-ip.sh.": {
- // required for fly.io to obtain a certificate for the website
- CNAME: []*dns.CNAME{
- {Target: "local-ip.sh.zzkxm3.flydns.net."},
- },
// will be filled in later when requesting the wildcard certificate
TXT: &dns.TXT{},
},
@@ -365,7 +364,6 @@ func (xip *Xip) handleDnsRequest(response dns.ResponseWriter, request *dns.Msg)
}
func (xip *Xip) StartServer() {
- log.Printf("Listening on %s\n", xip.server.Addr)
err := xip.server.ListenAndServe()
defer xip.server.Shutdown()
if err != nil {
@@ -383,6 +381,7 @@ func (xip *Xip) StartServer() {
log.Fatalf("Failed to start server: %s\n ", err.Error())
}
+ log.Printf("Listening on %s\n", xip.server.Addr)
}
func NewXip(port int) (xip *Xip) {