-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathproxy.go
More file actions
222 lines (192 loc) · 7.5 KB
/
Copy pathproxy.go
File metadata and controls
222 lines (192 loc) · 7.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
package rhttp
import (
"context"
"crypto/tls"
"log/slog"
"net"
"net/http"
"github.com/oktalz/reverse-http/internal/proxy"
)
// ProxyOptions configures a Proxy (the server-side counterpart to the
// reverse-http worker established by NewConnectionPool).
type ProxyOptions struct {
// TunnelTLSConfig is the TLS configuration for the worker-facing
// listener. Required.
//
// Mandatory shape:
//
// - Certificates / GetCertificate set so workers can verify the proxy.
// - ClientAuth set to tls.RequireAndVerifyClientCert.
// - ClientCAs populated with the CA(s) that signed legitimate worker
// certs.
//
// "h2" is added to NextProtos automatically if missing. Worker
// authentication is performed entirely by the TLS layer: any client
// cert that chains to ClientCAs is accepted. Finer-grained gating
// (CN allow-lists, OU matching, external lookup) belongs in
// OnWorkerAttach or in your Selector — inspect Worker.Certificate().
TunnelTLSConfig *tls.Config
// Selector picks a worker for each public request. Optional; default
// is least-in-flight. Use ProxyRoundRobin() for plain round-robin,
// ProxySelectorFunc for ad-hoc logic, or implement ProxySelector
// yourself.
Selector ProxySelector
// Logger receives tunnel-level diagnostic events (attach/detach,
// protocol errors). Per-request failures still surface to the public
// caller via the HTTP response. Optional; default is silent.
Logger *slog.Logger
// OnWorkerAttach / OnWorkerDetach run on tunnel-accept goroutines as
// workers come and go. Optional. Should not block.
OnWorkerAttach func(*ProxyWorker)
OnWorkerDetach func(*ProxyWorker)
// MaxConcurrentStreams advertised to each worker tunnel. Zero means
// the H2 default (100).
MaxConcurrentStreams uint32
}
// ProxySelector picks the worker that a given public request should be
// forwarded to. Returning nil produces a 503 to the public caller.
//
// Implementations must tolerate an empty workers slice. The slice is a
// snapshot — do not retain it past the Pick call.
type ProxySelector interface {
Pick(req *http.Request, workers []*ProxyWorker) *ProxyWorker
}
// ProxySelectorFunc adapts an ordinary function to ProxySelector.
type ProxySelectorFunc func(req *http.Request, workers []*ProxyWorker) *ProxyWorker
// Pick implements ProxySelector.
func (f ProxySelectorFunc) Pick(req *http.Request, workers []*ProxyWorker) *ProxyWorker {
return f(req, workers)
}
// ProxyLeastInFlight returns the default selector — pick the attached
// worker with the fewest currently-in-flight forwards.
func ProxyLeastInFlight() ProxySelector { return wrapSelector(proxy.LeastInFlight()) }
// ProxyRoundRobin returns a selector that picks workers in round-robin
// order. Counter is shared across the returned selector instance; build a
// fresh one per Proxy if you want isolated state.
func ProxyRoundRobin() ProxySelector { return wrapSelector(proxy.RoundRobin()) }
// ProxyWorker represents one attached reverse-http tunnel.
type ProxyWorker struct {
w *proxy.Worker
}
// RemoteAddr returns the worker's network address.
func (pw *ProxyWorker) RemoteAddr() net.Addr { return pw.w.RemoteAddr() }
// CommonName returns the worker certificate's Subject CN (empty if no
// client cert was presented). Convenience around Certificate().
func (pw *ProxyWorker) CommonName() string {
if c := pw.w.Certificate(); c != nil {
return c.Subject.CommonName
}
return ""
}
// Certificate returns the leaf client certificate the worker presented
// during mTLS. nil if the tunnel TLS config didn't require / verify a
// client cert. Selectors and hooks may inspect this for routing.
func (pw *ProxyWorker) Certificate() any { return pw.w.Certificate() }
// InFlight returns the number of currently-active outbound streams on this
// worker's tunnel. Selectors typically consult this.
func (pw *ProxyWorker) InFlight() int64 { return pw.w.InFlight() }
// Proxy is the reverse-http server. It accepts mTLS HTTP/2 tunnel
// connections from workers and exposes Handler / ListenAndServePublic for
// the public side.
type Proxy struct {
p *proxy.Proxy
}
// NewProxy constructs a Proxy. ListenAndServeTunnels must be called to
// start accepting workers; Handler / ListenAndServePublic exposes the
// public side. Both are safe for concurrent use.
func NewProxy(opts ProxyOptions) (*Proxy, error) {
innerOpts := proxy.Options{
TunnelTLSConfig: opts.TunnelTLSConfig,
Logger: opts.Logger,
MaxConcurrentStreams: opts.MaxConcurrentStreams,
}
if opts.Selector != nil {
innerOpts.Selector = unwrapSelector(opts.Selector)
}
if opts.OnWorkerAttach != nil {
hook := opts.OnWorkerAttach
innerOpts.OnWorkerAttach = func(w *proxy.Worker) { hook(&ProxyWorker{w: w}) }
}
if opts.OnWorkerDetach != nil {
hook := opts.OnWorkerDetach
innerOpts.OnWorkerDetach = func(w *proxy.Worker) { hook(&ProxyWorker{w: w}) }
}
inner, err := proxy.New(innerOpts)
if err != nil {
return nil, err
}
return &Proxy{p: inner}, nil
}
// Handler returns an http.Handler that forwards each request to a worker
// chosen by the configured Selector. Mount on any net/http server.
func (p *Proxy) Handler() http.Handler { return p.p.Handler() }
// ListenAndServeTunnels binds addr and accepts worker tunnels. Blocks until
// Shutdown or a listener error. The TLS config is ProxyOptions.TunnelTLSConfig
// with "h2" ensured in NextProtos.
func (p *Proxy) ListenAndServeTunnels(addr string) error {
return p.p.ListenAndServeTunnels(addr)
}
// ServeTunnelListener accepts tunnels on a pre-built listener. The listener
// must yield *tls.Conn with ALPN h2 negotiated.
func (p *Proxy) ServeTunnelListener(ln net.Listener) error {
return p.p.ServeTunnelListener(ln)
}
// ListenAndServePublic is a turnkey public listener — binds addr and serves
// Handler() under tlsConfig (pass nil for plain HTTP, e.g. behind a TLS
// terminator). Blocks until Shutdown or a listener error.
func (p *Proxy) ListenAndServePublic(addr string, tlsConfig *tls.Config) error {
return p.p.ListenAndServePublic(addr, tlsConfig)
}
// Workers returns a snapshot of attached workers. Slice is freshly-allocated;
// safe to retain.
func (p *Proxy) Workers() []*ProxyWorker {
inner := p.p.Workers()
out := make([]*ProxyWorker, len(inner))
for i, w := range inner {
out[i] = &ProxyWorker{w: w}
}
return out
}
// Shutdown stops accepting new tunnels, tears down attached ones (so in-flight
// public requests fail cleanly), and waits for accept goroutines to exit.
// Returns ctx.Err() if the deadline trips first. Safe to call multiple times.
func (p *Proxy) Shutdown(ctx context.Context) error { return p.p.Shutdown(ctx) }
// ---- adapter glue between the public ProxySelector and the internal one --
type selectorAdapter struct {
inner proxy.Selector
}
func (a *selectorAdapter) Pick(req *http.Request, workers []*ProxyWorker) *ProxyWorker {
inner := make([]*proxy.Worker, len(workers))
for i, w := range workers {
inner[i] = w.w
}
picked := a.inner.Pick(req, inner)
if picked == nil {
return nil
}
for _, w := range workers {
if w.w == picked {
return w
}
}
return nil
}
func wrapSelector(s proxy.Selector) ProxySelector {
return &selectorAdapter{inner: s}
}
func unwrapSelector(s ProxySelector) proxy.Selector {
if a, ok := s.(*selectorAdapter); ok {
return a.inner
}
return proxy.SelectorFunc(func(req *http.Request, inner []*proxy.Worker) *proxy.Worker {
wrapped := make([]*ProxyWorker, len(inner))
for i, w := range inner {
wrapped[i] = &ProxyWorker{w: w}
}
picked := s.Pick(req, wrapped)
if picked == nil {
return nil
}
return picked.w
})
}