Skip to content
File

Blob: gateway/gateway.go

go354 lines
1package gateway
2 
3import (
4 "context"
5 "crypto/tls"
6 "io"
7 "net"
8 "net/http"
9 "slices"
10 "strings"
11 "time"
12 
13 "go.miragespace.co/specter/spec/cipher"
14 "go.miragespace.co/specter/spec/protocol"
15 "go.miragespace.co/specter/spec/transport"
16 "go.miragespace.co/specter/spec/transport/q"
17 "go.miragespace.co/specter/spec/tun"
18 "go.miragespace.co/specter/timing"
19 "go.miragespace.co/specter/util"
20 "go.miragespace.co/specter/util/acceptor"
21 
22 "github.com/go-chi/chi/v5"
23 "github.com/go-chi/httprate"
24 "github.com/libp2p/go-yamux/v4"
25 "github.com/quic-go/quic-go"
26 "github.com/quic-go/quic-go/http3"
27 "go.uber.org/zap"
28 "go.uber.org/zap/zapcore"
29 "moul.io/zapfilter"
30)
31 
32type DeadlineReadWriteCloser interface {
33 io.ReadWriteCloser
34 SetReadDeadline(time.Time) error
35}
36 
37type InternalHandlers struct {
38 Overview http.Handler
39 Acme http.Handler
40 Chord http.Handler
41 TunnelServer http.Handler
42 Migrator http.Handler
43}
44 
45type Options struct {
46 TransportBufferSize int
47 ProxyBufferSize int
48}
49 
50type GatewayConfig struct {
51 Handlers InternalHandlers
52 PKIServer protocol.PKIService
53 TunnelServer tun.Server
54 HTTPListener net.Listener
55 H2Listener net.Listener
56 H3Listener q.Listener
57 Logger *zap.Logger
58 HandshakeHintFunc cipher.OnHandshakeFunc
59 AdminUser string
60 AdminPass string
61 RootDomains []string
62 GatewayPort int
63 Options Options
64}
65 
66type Gateway struct {
67 apexServer *apexServer
68 httpTunnelAcceptor *acceptor.HTTP2Acceptor
69 http3TunnelAcceptor *acceptor.HTTP3Acceptor
70 tcpApexAcceptor *acceptor.HTTP2Acceptor
71 quicApexAcceptor *acceptor.HTTP3Acceptor
72 tcpApexServer *http.Server
73 h2TunnelServer *http.Server
74 localApexServer *http.Server
75 httpServer *http.Server
76 quicApexServer *http3.Server
77 h3TunnelServer *http3.Server
78 altHeaders string
79 GatewayConfig
80}
81 
82func (c *GatewayConfig) startApex() bool {
83 return len(c.RootDomains) > 0
84}
85 
86func New(conf GatewayConfig) *Gateway {
87 if conf.PKIServer != nil {
88 conf.Logger.Info("Enabling client certificate issuance")
89 }
90 
91 g := &Gateway{
92 GatewayConfig: conf,
93 httpTunnelAcceptor: acceptor.NewH2Acceptor(conf.H2Listener),
94 tcpApexAcceptor: acceptor.NewH2Acceptor(conf.H2Listener),
95 http3TunnelAcceptor: acceptor.NewH3Acceptor(conf.H3Listener),
96 quicApexAcceptor: acceptor.NewH3Acceptor(conf.H3Listener),
97 }
98 
99 // filter out unproductive messages
100 filteredLogger := zap.New(zapfilter.NewFilteringCore(
101 g.Logger.Core(),
102 func(e zapcore.Entry, f []zapcore.Field) bool {
103 if strings.HasPrefix(e.Message, "http: URL query contains semicolon") {
104 return false
105 }
106 if strings.HasPrefix(e.Message, "suppressing panic for copyResponse error in test;") {
107 return false
108 }
109 return true
110 }),
111 )
112 proxyHandler := g.proxyHandler(util.GetStdLogger(filteredLogger, "httpProxy"))
113 
114 qCfg := &quic.Config{
115 HandshakeIdleTimeout: time.Second * 5,
116 KeepAlivePeriod: time.Second * 30,
117 MaxIdleTimeout: time.Second * 60,
118 }
119 
120 if conf.startApex() {
121 if conf.AdminUser == "" || conf.AdminPass == "" {
122 conf.Logger.Info("Missing credentials for internal endpoint, disabling /_internal endpoint")
123 }
124 apex := chi.NewRouter()
125 apex.Use(func(h http.Handler) http.Handler {
126 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127 g.appendHeaders(r.ProtoAtLeast(3, 0))(w.Header())
128 h.ServeHTTP(w, r)
129 })
130 })
131 g.apexServer = &apexServer{
132 handlers: conf.Handlers,
133 limiter: httprate.LimitAll(10, time.Second), // limit request to apex endpoint to 10 req/s
134 internalProxy: g.getInternalProxyHandler(),
135 pkiServer: conf.PKIServer,
136 authUser: conf.AdminUser,
137 authPass: conf.AdminPass,
138 }
139 g.apexServer.Mount(apex)
140 g.mountCgiHandler(apex)
141 g.tcpApexServer = &http.Server{
142 ReadHeaderTimeout: time.Second * 5,
143 Handler: apex,
144 ErrorLog: util.GetStdLogger(filteredLogger, "tcpApex"),
145 }
146 g.quicApexServer = &http3.Server{
147 QUICConfig: qCfg,
148 EnableDatagrams: false,
149 Handler: apex,
150 }
151 g.localApexServer = &http.Server{
152 Addr: "127.0.0.1:9999",
153 ReadHeaderTimeout: time.Second * 5,
154 Handler: apex,
155 ErrorLog: util.GetStdLogger(filteredLogger, "localApex"),
156 }
157 }
158 
159 g.h2TunnelServer = &http.Server{
160 ReadHeaderTimeout: time.Second * 5,
161 Handler: proxyHandler,
162 ErrorLog: util.GetStdLogger(filteredLogger, "h2Tunnel"),
163 }
164 g.h3TunnelServer = &http3.Server{
165 QUICConfig: qCfg,
166 EnableDatagrams: false,
167 Handler: proxyHandler,
168 }
169 g.httpServer = &http.Server{
170 ReadHeaderTimeout: 5 * time.Second,
171 ReadTimeout: 5 * time.Second,
172 WriteTimeout: 5 * time.Second,
173 IdleTimeout: 5 * time.Second,
174 Handler: g.httpRouter(),
175 ErrorLog: util.GetStdLogger(filteredLogger, "httpServer"),
176 }
177 g.altHeaders = generateAltHeaders(conf.GatewayPort)
178 
179 return g
180}
181 
182func (g *Gateway) AttachRouter(ctx context.Context, router *transport.StreamRouter) {
183 router.HandleChord(protocol.Stream_INTERNAL, nil, func(delegate *transport.StreamDelegate) {
184 g.tcpApexAcceptor.Handle(delegate)
185 })
186}
187 
188func (g *Gateway) MustStart(ctx context.Context) {
189 // provide application context
190 g.h2TunnelServer.BaseContext = func(l net.Listener) context.Context { return ctx }
191 g.httpServer.BaseContext = func(l net.Listener) context.Context { return ctx }
192 
193 go g.h2TunnelServer.Serve(g.httpTunnelAcceptor)
194 go g.h3TunnelServer.ServeListener(g.http3TunnelAcceptor)
195 
196 go g.acceptTCP(ctx)
197 go g.acceptQUIC(ctx)
198 
199 if g.startApex() {
200 g.tcpApexServer.BaseContext = func(l net.Listener) context.Context { return ctx }
201 g.localApexServer.BaseContext = func(l net.Listener) context.Context { return ctx }
202 
203 go g.tcpApexServer.Serve(g.tcpApexAcceptor)
204 go g.quicApexServer.ServeListener(g.quicApexAcceptor)
205 go g.localApexServer.ListenAndServe()
206 }
207 
208 if g.HTTPListener != nil {
209 g.Logger.Info("Enabling HTTP Handler for HTTP Connect and HTTPS Redirect", zap.String("listen", g.HTTPListener.Addr().String()))
210 go g.httpServer.Serve(g.HTTPListener)
211 }
212 
213 g.Logger.Info("gateway server started")
214}
215 
216func (g *Gateway) Close() {
217 g.httpTunnelAcceptor.Close()
218 
219 g.h2TunnelServer.Close()
220 g.h3TunnelServer.Close()
221 
222 if g.startApex() {
223 g.tcpApexAcceptor.Close()
224 g.quicApexAcceptor.Close()
225 
226 g.tcpApexServer.Close()
227 g.quicApexServer.Close()
228 
229 g.localApexServer.Close()
230 }
231}
232 
233func (g *Gateway) acceptTCP(ctx context.Context) {
234 for {
235 conn, err := g.H2Listener.Accept()
236 if err != nil {
237 return
238 }
239 tconn := conn.(*tls.Conn)
240 go g.handleH2Connection(ctx, tconn)
241 }
242}
243 
244func (g *Gateway) acceptQUIC(ctx context.Context) {
245 for {
246 conn, err := g.H3Listener.Accept(ctx)
247 if err != nil {
248 return
249 }
250 go g.handleH3Connection(ctx, conn)
251 }
252}
253 
254func (g *Gateway) handleH3Connection(ctx context.Context, q *quic.Conn) {
255 cs := q.ConnectionState().TLS
256 logger := g.Logger.With(
257 zap.Bool("via-quic", true),
258 zap.String("proto", cs.NegotiatedProtocol),
259 zap.String("tls.ServerName", cs.ServerName),
260 )
261 
262 if len(cs.ServerName) == 0 {
263 q.CloseWithError(0, "")
264 return
265 }
266 
267 if slices.Contains(g.RootDomains, cs.ServerName) {
268 logger.Debug("forwarding apex connection")
269 g.quicApexAcceptor.Handle(q)
270 return
271 }
272 
273 // maybe tunnel it
274 switch cs.NegotiatedProtocol {
275 case tun.ALPN(protocol.Link_TCP):
276 logger.Debug("forwarding tcp connection")
277 g.handleH3Multiplex(ctx, logger, q, cs.ServerName)
278 default:
279 logger.Debug("forwarding http connection")
280 g.http3TunnelAcceptor.Handle(q)
281 }
282}
283 
284func (g *Gateway) handleH3Multiplex(ctx context.Context, logger *zap.Logger, q *quic.Conn, host string) {
285 for {
286 stream, err := q.AcceptStream(ctx)
287 if err != nil {
288 return
289 }
290 go func(stream DeadlineReadWriteCloser) {
291 if err := g.forwardTCP(ctx, host, q.RemoteAddr().String(), stream); err == nil {
292 logger.Debug("forwarding tcp connection")
293 }
294 }(stream)
295 }
296}
297 
298func (g *Gateway) handleH2Connection(ctx context.Context, conn *tls.Conn) {
299 hsCtx, cancel := context.WithTimeout(ctx, timing.TLSHandshakeTimeout)
300 defer cancel()
301 if err := conn.HandshakeContext(hsCtx); err != nil {
302 conn.Close()
303 return
304 }
305 
306 cs := conn.ConnectionState()
307 logger := g.Logger.With(
308 zap.Bool("via-quic", false),
309 zap.String("proto", cs.NegotiatedProtocol),
310 zap.String("tls.ServerName", cs.ServerName),
311 )
312 
313 if len(cs.ServerName) == 0 {
314 conn.Close()
315 return
316 }
317 
318 if slices.Contains(g.RootDomains, cs.ServerName) {
319 logger.Debug("forwarding apex connection")
320 g.tcpApexAcceptor.Handle(conn)
321 return
322 }
323 
324 // maybe tunnel it
325 switch cs.NegotiatedProtocol {
326 case tun.ALPN(protocol.Link_TCP):
327 logger.Debug("forwarding tcp connection")
328 cfg := yamux.DefaultConfig()
329 cfg.LogOutput = io.Discard
330 session, err := yamux.Server(conn, cfg, nil)
331 if err != nil {
332 return
333 }
334 g.handleH2Multiplex(ctx, logger, session, cs.ServerName)
335 default:
336 logger.Debug("forwarding http connection")
337 g.httpTunnelAcceptor.Handle(conn)
338 }
339}
340 
341func (g *Gateway) handleH2Multiplex(ctx context.Context, logger *zap.Logger, session *yamux.Session, host string) {
342 for {
343 stream, err := session.AcceptStream()
344 if err != nil {
345 return
346 }
347 go func(stream DeadlineReadWriteCloser) {
348 if err := g.forwardTCP(ctx, host, session.RemoteAddr().String(), stream); err == nil {
349 logger.Debug("forwarding tcp connection")
350 }
351 }(stream)
352 }
353}