orchard
mirrorEvery site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.
blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics
1package tools
2
3// The fence between the open web and this machine.
4//
5// Every public tool here takes a url or a query from a model, and a model will
6// happily be talked into fetching whatever it is handed. This site now sits on
7// orchard-edge, so an unguarded fetch of http://orchard-auth:8000 or
8// http://127.0.0.1 reaches the estate from inside, past Caddy and past the
9// tunnel, which is the one place nothing is expecting an untrusted caller.
10//
11// The check is on the address actually dialled rather than on the string, so a
12// redirect to an internal host and a name that resolves to one are both caught.
13// The orchard_ tools deliberately do not go through this: they are the sanctioned
14// way in, they name their host, and they carry the caller's own session.
15
16import (
17 "fmt"
18 "net"
19 "net/http"
20 "net/url"
21 "strings"
22 "syscall"
23 "time"
24)
25
26// publicClient refuses to connect to anything that is not a public address.
27//
28// Control runs after the name is resolved and before the socket is connected,
29// which is the only point where the decision can be made on the address that
30// will really be used. Checking the hostname instead leaves DNS rebinding open.
31func publicClient(timeout time.Duration) *http.Client {
32 d := &net.Dialer{
33 Timeout: 10 * time.Second,
34 KeepAlive: 30 * time.Second,
35 Control: func(network, address string, _ syscall.RawConn) error {
36 host, _, err := net.SplitHostPort(address)
37 if err != nil {
38 return fmt.Errorf("refusing an address that cannot be parsed")
39 }
40 ip := net.ParseIP(host)
41 if ip == nil {
42 return fmt.Errorf("refusing an address that is not an ip")
43 }
44 if !isPublicIP(ip) {
45 return fmt.Errorf("refusing to fetch %s, which is on this machine or its network", ip)
46 }
47 return nil
48 },
49 }
50 return &http.Client{
51 Timeout: timeout,
52 Transport: &http.Transport{DialContext: d.DialContext, ForceAttemptHTTP2: true},
53 }
54}
55
56func isPublicIP(ip net.IP) bool {
57 if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() ||
58 ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
59 ip.IsInterfaceLocalMulticast() || ip.IsMulticast() {
60 return false
61 }
62 // 100.64.0.0/10, which is not covered by IsPrivate and is where a carrier
63 // grade NAT and most of Tailscale live.
64 if v4 := ip.To4(); v4 != nil && v4[0] == 100 && v4[1] >= 64 && v4[1] <= 127 {
65 return false
66 }
67 // The cloud metadata address, which is public by every other measure and is
68 // the first thing anything like this gets pointed at.
69 if ip.Equal(net.IPv4(169, 254, 169, 254)) {
70 return false
71 }
72 return true
73}
74
75// publicURL is the check on the string, done before the dial so an obviously
76// wrong scheme is refused with something a model can act on rather than with a
77// connection error.
78func publicURL(raw string) (string, error) {
79 raw = strings.TrimSpace(raw)
80 u, err := url.Parse(raw)
81 if err != nil {
82 return "", fmt.Errorf("that is not a url")
83 }
84 switch strings.ToLower(u.Scheme) {
85 case "http", "https":
86 case "":
87 return "", fmt.Errorf("the url needs to start with https://")
88 case "file":
89 // The one a model reaches for when it is thinking about an attachment,
90 // so the message says where the file actually is.
91 return "", fmt.Errorf("there is no filesystem to read here, and an attached file is already in this conversation, so read it there rather than fetching it")
92 default:
93 return "", fmt.Errorf("only http and https can be fetched, not %s", u.Scheme)
94 }
95 if u.Host == "" {
96 return "", fmt.Errorf("the url has no host")
97 }
98 // A bare name with no dot is a container on the bridge, and the estate is
99 // reached with the orchard tools rather than by fetching it.
100 host := u.Hostname()
101 if !strings.Contains(host, ".") && net.ParseIP(host) == nil {
102 return "", fmt.Errorf("%q is not a public address, and this machine's own services are read with the orchard tools", host)
103 }
104 return u.String(), nil
105}