carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package console
2
3// Who sent this X-Forwarded-For (discussion 214, item 5).
4//
5// The console listens on 127.0.0.1, behind the edge on its own box, so every
6// proxied request arrives from loopback and the client's address is the
7// edge's X-Forwarded-For. But loopback is not the edge alone: any tenant
8// process on the console's box can connect to 127.0.0.1:8090 and send any
9// X-Forwarded-For it likes, one fresh address per request, past every per-IP
10// limit on the device-code login. So a loopback peer's chain is believed only
11// with the edge's signature over it (internal/edgeproof), checked against the
12// deployment's mesh key, which tenants cannot read.
13//
14// Rollout needs one more rule. An edge that predates the proof sends none,
15// and refusing its chains would put every client in the deployment into the
16// single 127.0.0.1 rate-limit bucket — ten login starts a minute for
17// everybody — until the edge caught up. The console therefore keeps the old
18// trust until it has seen ONE valid proof, and from then on requires one for
19// good. Only the real edge can produce that proof, and the moment is stored
20// (edge_proof_seen) so a restart does not reopen the window. Rolling the edge
21// back past the proof after that is the one way to land in the shared
22// bucket; the console logs it once when it happens.
23//
24// The latch must only close on a proof from THIS box's edge. Far edges send
25// none across a backhaul hop (internal/edge's backhaulProxy) for exactly
26// that reason: an older home edge would pass a far edge's proof straight
27// through, and the console would latch while its own edge still signed
28// nothing. The proof also binds the request's Host, so a tenant app on this
29// box cannot replay the proofs it receives on its own traffic here.
30
31import (
32 "context"
33 "database/sql"
34 "log"
35 "net"
36 "net/http"
37 "sync"
38 "sync/atomic"
39 "time"
40
41 "github.com/carlosframework/platform/internal/edgeproof"
42)
43
44// edgeProofKeyTTL is how long one read of the mesh keys stands. The edge
45// polls the same objects every 30s, so a rotation reaches both sides on the
46// same scale.
47const edgeProofKeyTTL = 30 * time.Second
48
49// edgeProofKeyTimeout bounds one key read: it runs on the login path, and a
50// hung store must cost a bounded wait, not a wedged door.
51const edgeProofKeyTimeout = 5 * time.Second
52
53// edgeProofState is the cached key pair and the seen latch. Zero value is
54// ready to use.
55type edgeProofState struct {
56 mu sync.Mutex
57 at time.Time
58 current, next []byte
59
60 // loaded is set once the stored mark has been read; seen is the latch.
61 loaded atomic.Bool
62 seen atomic.Bool
63 warned atomic.Bool
64}
65
66// forwardedByEdge reports whether a loopback peer's X-Forwarded-For chain
67// (xff, its lines comma-joined) came from the edge.
68func (s *Server) forwardedByEdge(r *http.Request, xff string) bool {
69 st := &s.edgeProof
70 if proof := r.Header.Get(edgeproof.Header); proof != "" {
71 current, next := s.edgeProofKeys(r.Context())
72 if edgeproof.Verify(current, next, r.Host, xff, proof, time.Now()) {
73 s.markEdgeProofSeen()
74 return true
75 }
76 }
77 if !s.edgeProofSeen() {
78 return true // no proof yet from this deployment's edge: the old trust
79 }
80 if st.warned.CompareAndSwap(false, true) {
81 log.Printf("console: a loopback request carried X-Forwarded-For with no valid edge proof; counting it against the peer address. Expected now and then from a process on this box; on every request it means the edge no longer signs (rolled back?) and every client shares one rate limit")
82 }
83 return false
84}
85
86// edgeProofSeen answers the latch, reading the stored row once per process.
87// A read failure counts as seen for this request and is retried on the
88// next: a database blip must not be the way back to the old trust.
89func (s *Server) edgeProofSeen() bool {
90 st := &s.edgeProof
91 if st.seen.Load() {
92 return true
93 }
94 if st.loaded.Load() {
95 return false
96 }
97 seen, err := s.store.EdgeProofSeen()
98 if err != nil {
99 log.Printf("console: edge proof: read the seen mark: %v", err)
100 return true
101 }
102 if seen {
103 st.seen.Store(true)
104 }
105 st.loaded.Store(true)
106 return seen
107}
108
109func (s *Server) markEdgeProofSeen() {
110 st := &s.edgeProof
111 if st.seen.Load() {
112 return
113 }
114 if err := s.store.MarkEdgeProofSeen(time.Now()); err != nil {
115 log.Printf("console: edge proof: store the seen mark: %v", err)
116 } else {
117 log.Printf("console: edge proof: first signed request from the edge — X-Forwarded-For from loopback now needs a proof")
118 }
119 st.seen.Store(true)
120}
121
122// edgeProofKeys is the cached mesh key pair. With no deployment bucket there
123// are no keys, nothing verifies, and the latch never closes — the old trust,
124// on a console that cannot have an edge signing for it anyway. A failed read
125// keeps the last good pair.
126func (s *Server) edgeProofKeys(ctx context.Context) (current, next []byte) {
127 st := &s.edgeProof
128 if s.cfg.Releases == nil {
129 return nil, nil
130 }
131 st.mu.Lock()
132 defer st.mu.Unlock()
133 if !st.at.IsZero() && time.Since(st.at) < edgeProofKeyTTL {
134 return st.current, st.next
135 }
136 ctx, cancel := context.WithTimeout(ctx, edgeProofKeyTimeout)
137 defer cancel()
138 c, n, err := edgeproof.LoadKeys(ctx, s.cfg.Releases)
139 // Stamped either way, so a store that is down is asked once per TTL
140 // rather than on every login request.
141 st.at = time.Now()
142 if err != nil {
143 log.Printf("console: edge proof: %v", err)
144 return st.current, st.next
145 }
146 st.current, st.next = c, n
147 return c, n
148}
149
150// isLoopbackPeer reports whether host (a RemoteAddr's host part) is this box.
151func isLoopbackPeer(host string) bool {
152 ip := net.ParseIP(host)
153 return ip != nil && ip.IsLoopback()
154}
155
156// EdgeProofSeen reports whether this console has ever received a request
157// carrying a valid edge forwarded-for proof.
158func (s *Store) EdgeProofSeen() (bool, error) {
159 var at int64
160 err := s.db.QueryRow(`SELECT seen_at FROM edge_proof_seen WHERE id = 1`).Scan(&at)
161 if err == sql.ErrNoRows {
162 return false, nil
163 }
164 return err == nil, err
165}
166
167// MarkEdgeProofSeen records the first valid proof. Later calls keep the
168// first moment.
169func (s *Store) MarkEdgeProofSeen(now time.Time) error {
170 _, err := s.db.Exec(`INSERT OR IGNORE INTO edge_proof_seen (id, seen_at) VALUES (1, ?)`, now.Unix())
171 return err
172}
173