Commit d42eec7

Antonio Mika  ·  2026-10-11 16:45:03 -0400 EDT
parent 874bfe4
feat(pico): check where a domain points in dns-check and pin pico-ssh dns
6 files changed,  +147, -8
+1, -0
......@@ -352,6 +352,7 @@ services:
352352 - all
353353 logging: *default-logging
354354 pico-ssh:
355+ dns: 1.1.1.1
355356 networks:
356357 pico:
357358 aliases:
+11, -4
......@@ -43,7 +43,11 @@ type Cmd struct {
4343 Dbpool db.DB
4444 Write bool
4545
46- lookupTXT func(host, space string) ([]string, error)
46+ // DomainTargets are the hosts dns-check expects a custom domain to point at.
47+ DomainTargets []string
48+
49+ lookupTXT func(host, space string) ([]string, error)
50+ lookupHost func(host string) (cname string, addrs []string, err error)
4751 }
4852
4953 func (c *Cmd) output(out string) {
......@@ -58,7 +62,7 @@ func (c *Cmd) help() {
5862 helpStr += "access-logs - fetch access logs from the last 30 days\n"
5963 helpStr += "chat - IRC chat (must enable pty with `-t` to the SSH command)\n"
6064 helpStr += "not-found - return all status 404 requests for a host (hostname.com [year|month])\n"
61- helpStr += "dns-check - check a domain's TXT records for pgs, prose and tuns custom domains (hostname.com or *.hostname.com)\n"
65+ helpStr += "dns-check - check a domain's CNAME/A and TXT records for pgs, prose and tuns custom domains (hostname.com or *.hostname.com)\n"
6266 c.output(helpStr)
6367 }
6468
......@@ -177,8 +181,9 @@ func (c *Cmd) logs(ctx context.Context) error {
177181 }
178182
179183 type CliHandler struct {
180- DBPool db.DB
181- Logger *slog.Logger
184+ DBPool db.DB
185+ Logger *slog.Logger
186+ DomainTargets []string
182187 }
183188
184189 func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
......@@ -250,6 +255,8 @@ func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
250255 Log: log,
251256 Dbpool: dbpool,
252257 Write: false,
258+
259+ DomainTargets: handler.DomainTargets,
253260 }
254261
255262 cmd := strings.TrimSpace(args[0])
+13, -0
......@@ -12,6 +12,8 @@ type Config struct {
1212 Space string
1313 TunsSecret string
1414 Logger *slog.Logger
15+ // CustomDomainTargets are the hosts a custom domain can point at.
16+ CustomDomainTargets []string
1517 }
1618
1719 func NewConfig(service string) *Config {
......@@ -24,5 +26,16 @@ func NewConfig(service string) *Config {
2426 Space: "pico",
2527 Logger: shared.CreateLogger(service, withPipe),
2628 TunsSecret: tuns,
29+ CustomDomainTargets: []string{
30+ hostOnly(shared.GetEnv("PGS_DOMAIN", "pgs.sh")),
31+ hostOnly(shared.GetEnv("PROSE_DOMAIN", "prose.sh")),
32+ "tuns.sh",
33+ "nue.tuns.sh",
34+ },
2735 }
2836 }
37+
38+func hostOnly(domain string) string {
39+ host, _, _ := strings.Cut(domain, ":")
40+ return host
41+}
+61, -2
......@@ -5,6 +5,8 @@ import (
55 "encoding/hex"
66 "errors"
77 "fmt"
8+ "net"
9+ "slices"
810 "strings"
911
1012 "github.com/picosh/pico/pkg/db"
......@@ -107,8 +109,59 @@ func checkTunsRecords(c *Cmd, records []string) string {
107109 return fmt.Sprintf("%s is invalid: no record is your username or the SHA256 fingerprint of one of your keys", strings.Join(quoted, ", "))
108110 }
109111
110-// dnsCheck reports, for each service, whether domain has a TXT record and
111-// whether the service would accept it.
112+// lookupHost resolves host and returns its canonical name, or "" when it has
113+// no CNAME.
114+func lookupHost(host string) (string, []string, error) {
115+ addrs, err := net.LookupHost(host)
116+ if err != nil {
117+ return "", nil, err
118+ }
119+ cname, err := net.LookupCNAME(host)
120+ if err != nil {
121+ return "", addrs, nil
122+ }
123+ cname = strings.TrimSuffix(cname, ".")
124+ if strings.EqualFold(cname, host) {
125+ cname = ""
126+ }
127+ return cname, addrs, nil
128+}
129+
130+// checkPointer reports what host resolves to and which of the service
131+// domains it points at. Addresses are compared so a CNAME to a regional
132+// name or an apex A record both count.
133+func (c *Cmd) checkPointer(host string, lookup func(string) (string, []string, error)) string {
134+ cname, addrs, err := lookup(host)
135+ var dnsErr *net.DNSError
136+ if errors.As(err, &dnsErr) && dnsErr.IsNotFound {
137+ return fmt.Sprintf("%s: no CNAME or A record", host)
138+ }
139+ if err != nil {
140+ return fmt.Sprintf("%s: lookup failed: %v", host, err)
141+ }
142+
143+ line := fmt.Sprintf("%s: resolves to %s", host, strings.Join(addrs, ", "))
144+ if cname != "" {
145+ line = fmt.Sprintf("%s: CNAME %s, resolves to %s", host, cname, strings.Join(addrs, ", "))
146+ }
147+ matches := []string{}
148+ for _, target := range c.DomainTargets {
149+ _, targetAddrs, err := lookup(target)
150+ if err != nil {
151+ continue
152+ }
153+ if slices.ContainsFunc(addrs, func(a string) bool { return slices.Contains(targetAddrs, a) }) {
154+ matches = append(matches, target)
155+ }
156+ }
157+ if len(matches) == 0 {
158+ return fmt.Sprintf("%s, which is not %s", line, strings.Join(c.DomainTargets, ", "))
159+ }
160+ return fmt.Sprintf("%s, which is %s", line, strings.Join(matches, ", "))
161+}
162+
163+// dnsCheck reports what domain points at and, for each service, whether it
164+// has a TXT record the service would accept.
112165 func (c *Cmd) dnsCheck(domain string) error {
113166 domain = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(domain)), ".")
114167 base, wildcard := strings.CutPrefix(domain, "*.")
......@@ -128,6 +181,12 @@ func (c *Cmd) dnsCheck(domain string) error {
128181 host = hex.EncodeToString(label) + "." + base
129182 }
130183
184+ resolve := c.lookupHost
185+ if resolve == nil {
186+ resolve = lookupHost
187+ }
188+ c.output(c.checkPointer(host, resolve))
189+
131190 for _, svc := range customDomainServices {
132191 name := router.TXTRecordName(host, svc.space)
133192 if wildcard && !svc.wildcard {
+58, -0
......@@ -6,6 +6,7 @@ import (
66 "fmt"
77 "io"
88 "log/slog"
9+ "net"
910 "strings"
1011 "testing"
1112
......@@ -124,6 +125,9 @@ func TestDNSCheck(t *testing.T) {
124125 Session: session,
125126 Log: slog.New(slog.DiscardHandler),
126127 Dbpool: dbpool,
128+ lookupHost: func(host string) (string, []string, error) {
129+ return "", nil, &net.DNSError{IsNotFound: true}
130+ },
127131 lookupTXT: func(host, space string) ([]string, error) {
128132 if tt.domain == "" && host != "example.com" {
129133 t.Fatalf("looked up %q", host)
......@@ -153,6 +157,60 @@ func TestDNSCheck(t *testing.T) {
153157 })
154158 }
155159
160+ pointers := []struct {
161+ name string
162+ hosts map[string][]string
163+ cname string
164+ want string
165+ }{
166+ {"none", map[string][]string{}, "", "example.com: no CNAME or A record"},
167+ {"cname to a region", map[string][]string{
168+ "example.com": {"10.0.0.3"},
169+ "pgs.sh": {"10.0.0.1"},
170+ "prose.sh": {"10.0.0.2"},
171+ "tuns.sh": {"10.0.0.3"},
172+ "nue.tuns.sh": {"10.0.0.4"},
173+ }, "tuns.pico.sh", "example.com: CNAME tuns.pico.sh, resolves to 10.0.0.3, which is tuns.sh"},
174+ {"apex A", map[string][]string{
175+ "example.com": {"10.0.0.9", "10.0.0.1"},
176+ "pgs.sh": {"10.0.0.1"},
177+ }, "", "example.com: resolves to 10.0.0.9, 10.0.0.1, which is pgs.sh"},
178+ {"elsewhere", map[string][]string{
179+ "example.com": {"10.0.0.9"},
180+ "pgs.sh": {"10.0.0.1"},
181+ }, "", "example.com: resolves to 10.0.0.9, which is not pgs.sh, prose.sh, tuns.sh, nue.tuns.sh"},
182+ }
183+ for _, tt := range pointers {
184+ t.Run("pointer "+tt.name, func(t *testing.T) {
185+ session := &fakeSession{}
186+ cmd := &Cmd{
187+ User: me,
188+ Session: session,
189+ Dbpool: dbpool,
190+ DomainTargets: []string{"pgs.sh", "prose.sh", "tuns.sh", "nue.tuns.sh"},
191+ lookupHost: func(host string) (string, []string, error) {
192+ addrs, ok := tt.hosts[host]
193+ if !ok {
194+ return "", nil, &net.DNSError{IsNotFound: true}
195+ }
196+ if host == "example.com" {
197+ return tt.cname, addrs, nil
198+ }
199+ return "", addrs, nil
200+ },
201+ lookupTXT: func(host, space string) ([]string, error) {
202+ return nil, router.ErrNoTXTRecord
203+ },
204+ }
205+ if err := cmd.dnsCheck("example.com"); err != nil {
206+ t.Fatal(err)
207+ }
208+ if out := session.String(); !strings.Contains(out, tt.want+"\r\n") {
209+ t.Errorf("missing %q in output:\n%s", tt.want, out)
210+ }
211+ })
212+ }
213+
156214 cmd := &Cmd{User: me, Session: &fakeSession{}, Dbpool: dbpool}
157215 for _, bad := range []string{"https://example.com/", "*.", "a.*.example.com"} {
158216 if err := cmd.dnsCheck(bad); err == nil {
+3, -2
......@@ -62,8 +62,9 @@ func StartSshServer() {
6262 )
6363
6464 cliHandler := &CliHandler{
65- Logger: logger,
66- DBPool: dbpool,
65+ Logger: logger,
66+ DBPool: dbpool,
67+ DomainTargets: cfg.CustomDomainTargets,
6768 }
6869
6970 sshAuth := shared.NewSshAuthHandler(dbpool, logger, "pico")