eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1package tunnel
2
3// The relay half of the gateway (docs/relay-mode.md, issue #738 Phase 1):
4// stable, keypair-claimed hostnames with a disconnect grace window. Trials
5// (gateway.go) stay untouched; a relay differs in exactly three ways — its
6// hostname belongs to its keypair, it has no 24-hour age-out, and a dropped
7// connection puts the hostname to sleep instead of retiring it.
8
9import (
10 "context"
11 "crypto/rand"
12 "log"
13 "math/big"
14 "time"
15
16 "github.com/coder/websocket"
17)
18
19func (g *Gateway) maxRelays() int { return orInt(g.MaxRelays, 200) }
20func (g *Gateway) claimIdle() time.Duration {
21 if g.ClaimIdle > 0 {
22 return g.ClaimIdle
23 }
24 return 45 * 24 * time.Hour
25}
26
27// StartClaimJanitor frees claims whose owners haven't connected for ClaimIdle
28// (default 45 days), hourly. Currently-connected tunnels are never swept, no
29// matter how old their last_seen — Touch only runs at claim time.
30func (g *Gateway) StartClaimJanitor() {
31 if g.ClaimsDB == nil {
32 return
33 }
34 go func() {
35 for range time.Tick(time.Hour) {
36 g.mu.Lock()
37 keep := make(map[string]bool, len(g.tunnels))
38 for h := range g.tunnels {
39 keep[h] = true
40 }
41 g.mu.Unlock()
42 freed, err := g.ClaimsDB.SweepIdle(g.claimIdle(), keep)
43 if err != nil {
44 log.Printf("tunnel: claim janitor: %v", err)
45 continue
46 }
47 for _, h := range freed {
48 log.Printf("tunnel: claim janitor freed %s (idle > %s)", h, g.claimIdle())
49 }
50 }
51 }()
52}
53func (g *Gateway) relayGrace() time.Duration {
54 if g.RelayGrace > 0 {
55 return g.RelayGrace
56 }
57 return 15 * time.Minute
58}
59
60// handleRelayRegister runs after a hello with Mode "relay": challenge the
61// client to prove its keypair, look up (or mint) its hostname, then serve the
62// same probe/publish/hold flow trials get — minus the age-out, plus the
63// sleep-on-disconnect grace.
64func (g *Gateway) handleRelayRegister(ctx context.Context, c *websocket.Conn, ip string) {
65 if g.RelayDomain == "" || g.ClaimsDB == nil {
66 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "this gateway doesn't offer relay hostnames"})
67 c.Close(websocket.StatusPolicyViolation, "no relay support")
68 return
69 }
70
71 nonce, err := randToken(24)
72 if err != nil {
73 return
74 }
75 if err := writeJSON(ctx, c, ControlMsg{Type: "challenge", Nonce: nonce}); err != nil {
76 return
77 }
78 var claim ControlMsg
79 if err := readJSON(ctx, c, &claim); err != nil || claim.Type != "claim" {
80 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "expected claim"})
81 return
82 }
83 if err := VerifyClaimSig(claim.PubKey, nonce, claim.Sig); err != nil {
84 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "claim rejected: " + err.Error()})
85 c.Close(websocket.StatusPolicyViolation, "bad claim")
86 return
87 }
88
89 host, err := g.ClaimsDB.HostFor(claim.PubKey)
90 if err != nil {
91 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "claims store unavailable"})
92 return
93 }
94 if host == "" {
95 // A brand-new claim pays the same admission toll as a trial; a
96 // reconnect of an existing claim never does (a flaky home line must
97 // not rate-limit itself out of its own hostname).
98 if err := g.admit(ip); err != nil {
99 writeJSON(ctx, c, ControlMsg{Type: "error", Error: err.Error()})
100 return
101 }
102 count := 0
103 g.ClaimsDB.sql.QueryRow(`SELECT COUNT(*) FROM relay_claims`).Scan(&count)
104 if count >= g.maxRelays() {
105 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "this gateway has no free relay hostnames"})
106 return
107 }
108 if host, err = g.newRelayHost(); err != nil {
109 writeJSON(ctx, c, ControlMsg{Type: "error", Error: err.Error()})
110 return
111 }
112 if err := g.ClaimsDB.Claim(host, claim.PubKey); err != nil {
113 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "couldn't record the claim, try again"})
114 return
115 }
116 log.Printf("tunnel: relay %s claimed (from %s)", host, ip)
117 }
118 g.ClaimsDB.Touch(claim.PubKey)
119
120 t := &liveTunnel{host: host, control: c, created: time.Now(), closed: make(chan struct{}),
121 relay: true, pubkey: claim.PubKey}
122 t.proxy = g.proxyFor(t)
123
124 // Reattach: replace a sleeping (or superseded) tunnel for this host.
125 g.mu.Lock()
126 if old := g.tunnels[host]; old != nil {
127 if old.grace != nil {
128 old.grace.Stop()
129 }
130 // A previous live connection for the same key is superseded — the
131 // newest device wins, matching how people actually move machines.
132 go g.drop(old)
133 }
134 g.tunnels[host] = t
135 g.mu.Unlock()
136
137 if err := g.probeBackend(ctx, t); err != nil {
138 msg := "only Eleven instances can be relayed: " + err.Error()
139 writeJSON(ctx, c, ControlMsg{Type: "error", Error: msg})
140 c.Close(websocket.StatusPolicyViolation, "refused")
141 g.drop(t)
142 log.Printf("tunnel: relay %s refused (probe): %v", host, err)
143 return
144 }
145 if g.OnRegister != nil {
146 if err := g.OnRegister(host); err != nil {
147 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "couldn't publish the address, try again"})
148 g.drop(t)
149 log.Printf("tunnel: relay register %s: %v", host, err)
150 return
151 }
152 }
153 log.Printf("tunnel: relay %s connected (from %s)", host, ip)
154 if err := writeJSON(ctx, c, ControlMsg{Type: "ready", Host: host}); err != nil {
155 g.sleepTunnel(t)
156 return
157 }
158
159 // Hold: no age-out; re-probe like trials.
160 reprobe := time.NewTicker(10 * time.Minute)
161 defer reprobe.Stop()
162 probeFails := 0
163 readErr := make(chan error, 1)
164 go func() {
165 for {
166 if _, _, err := c.Read(ctx); err != nil {
167 readErr <- err
168 return
169 }
170 }
171 }()
172 for {
173 select {
174 case <-readErr:
175 g.sleepTunnel(t)
176 log.Printf("tunnel: relay %s sleeping (disconnected; grace %s)", host, g.relayGrace())
177 return
178 case <-reprobe.C:
179 if err := g.probeBackend(ctx, t); err != nil {
180 probeFails++
181 if probeFails >= 2 {
182 writeJSON(ctx, c, ControlMsg{Type: "error", Error: "the relayed backend stopped looking like an Eleven instance"})
183 g.drop(t)
184 log.Printf("tunnel: relay %s dropped (re-probe): %v", host, err)
185 return
186 }
187 } else {
188 probeFails = 0
189 }
190 case <-t.closed:
191 return
192 case <-ctx.Done():
193 g.sleepTunnel(t)
194 return
195 }
196 }
197}
198
199// sleepTunnel keeps a relay's hostname routable-but-503 for the grace window,
200// then retires it fully. The claim itself persists either way — the hostname
201// still belongs to the keypair and comes back on the next connect.
202func (g *Gateway) sleepTunnel(t *liveTunnel) {
203 g.mu.Lock()
204 if g.tunnels[t.host] != t { // superseded by a reattach
205 g.mu.Unlock()
206 return
207 }
208 t.sleeping = true
209 t.grace = time.AfterFunc(g.relayGrace(), func() { g.drop(t) })
210 g.mu.Unlock()
211 t.control.CloseNow()
212}
213
214func (g *Gateway) newRelayHost() (string, error) {
215 for range 20 {
216 slug := make([]byte, 6)
217 for i := range slug {
218 n, err := rand.Int(rand.Reader, big.NewInt(int64(len(slugAlphabet))))
219 if err != nil {
220 return "", err
221 }
222 slug[i] = slugAlphabet[n.Int64()]
223 }
224 host := "r-" + string(slug) + "." + g.RelayDomain
225 if owner, err := g.ClaimsDB.Owner(host); err != nil {
226 return "", err
227 } else if owner == "" {
228 return host, nil
229 }
230 }
231 return "", errNoFreeHost
232}
233