main
go 443 lines 9.93 KB
Raw
1 package sdk
2
3 import (
4 "bytes"
5 "context"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "sync"
11 "sync/atomic"
12 "time"
13
14 "github.com/rs/zerolog/log"
15
16 "github.com/gosuda/portal-tunnel/v2/types"
17 )
18
19 func ProxyExposure(ctx context.Context, exposure *Exposure) error {
20 defer exposure.Close()
21 if len(exposure.ActiveRelayURLs()) == 0 {
22 return errors.New("no relay URLs provided")
23 }
24
25 cfg := exposure.Config()
26 identity := cfg.Identity
27 tcpTarget := cfg.TargetAddr
28 udpTarget := cfg.UDPAddr
29 udpEnabled := udpTarget != ""
30
31 log.Info().
32 Str("release_version", types.ReleaseVersion).
33 Str("tcp_target", tcpTarget).
34 Str("service_name", identity.Name).
35 Strs("relays", exposure.ActiveRelayURLs()).
36 Msg("starting portal tunnel; public URLs will be logged as relays become ready")
37 if udpEnabled {
38 log.Info().
39 Str("udp_target", udpTarget).
40 Str("service_name", identity.Name).
41 Msg("udp relay enabled")
42 }
43
44 var connWG sync.WaitGroup
45 var connCount atomic.Int64
46 var udpErrCh chan error
47
48 if udpEnabled {
49 udpErrCh = make(chan error, 1)
50 go func() {
51 if err := runUDPProxy(ctx, exposure, udpTarget); err != nil && ctx.Err() == nil {
52 udpErrCh <- err
53 _ = exposure.Close()
54 }
55 }()
56 }
57
58 go func() {
59 <-ctx.Done()
60 _ = exposure.Close()
61 }()
62
63 waitErr := proxyRelayConnections(ctx, exposure, tcpTarget, &connWG, &connCount)
64 if waitErr != nil {
65 _ = exposure.Close()
66 }
67
68 var udpErr error
69 if udpErrCh != nil {
70 select {
71 case udpErr = <-udpErrCh:
72 default:
73 }
74 }
75
76 closeErr := exposure.Close()
77 if waitErr != nil {
78 log.Error().Err(waitErr).Msg("relay supervisor exited with error")
79 }
80 if udpErr != nil {
81 log.Error().Err(udpErr).Msg("udp proxy exited with error")
82 }
83 if closeErr != nil {
84 log.Warn().Err(closeErr).Msg("relay shutdown completed with cleanup errors")
85 }
86
87 if ctx.Err() != nil {
88 log.Info().Msg("tunnel shutting down")
89 }
90
91 done := make(chan struct{})
92 go func() {
93 connWG.Wait()
94 close(done)
95 }()
96
97 select {
98 case <-done:
99 case <-time.After(5 * time.Second):
100 log.Warn().Msg("tunnel shutdown timeout; connections still active")
101 }
102
103 log.Info().Msg("tunnel shutdown complete")
104 return errors.Join(waitErr, udpErr, closeErr)
105 }
106
107 func proxyRelayConnections(ctx context.Context, exposure *Exposure, localAddr string, connWG *sync.WaitGroup, connCount *atomic.Int64) error {
108 for {
109 relayConn, err := exposure.Accept()
110 if err != nil {
111 switch {
112 case errors.Is(err, context.Canceled):
113 return nil
114 case ctx.Err() != nil:
115 return ctx.Err()
116 case errors.Is(err, net.ErrClosed):
117 return errors.New("all relay listeners stopped")
118 default:
119 return err
120 }
121 }
122
123 connID := connCount.Add(1)
124 log.Info().
125 Int64("conn_id", connID).
126 Str("remote_addr", relayConn.RemoteAddr().String()).
127 Msg("accepted relay connection")
128
129 connWG.Add(1)
130 go func(connID int64, relayConn net.Conn) {
131 defer connWG.Done()
132 if err := proxyConnection(ctx, localAddr, relayConn); err != nil {
133 log.Debug().Err(err).Int64("conn_id", connID).Msg("proxy connection closed with an I/O error")
134 }
135 log.Info().Int64("conn_id", connID).Msg("proxy connection closed")
136 }(connID, relayConn)
137 }
138 }
139
140 var bufferPool = sync.Pool{
141 New: func() any {
142 b := make([]byte, 64*1024)
143 return &b
144 },
145 }
146
147 func proxyConnection(ctx context.Context, localAddr string, relayConn net.Conn) error {
148 defer relayConn.Close()
149
150 dialer := &net.Dialer{Timeout: 5 * time.Second}
151 localConn, err := dialer.DialContext(ctx, "tcp", localAddr)
152 if err != nil {
153 return writeEmptyHTTPResponse(relayConn)
154 }
155 defer localConn.Close()
156
157 errCh := make(chan error, 2)
158 stopCh := make(chan struct{})
159
160 go func() {
161 select {
162 case <-ctx.Done():
163 _ = relayConn.Close()
164 _ = localConn.Close()
165 case <-stopCh:
166 }
167 }()
168
169 go func() {
170 bufPtr := bufferPool.Get().(*[]byte)
171 defer bufferPool.Put(bufPtr)
172 _, err := io.CopyBuffer(localConn, relayConn, *bufPtr)
173 if tcpConn, ok := localConn.(*net.TCPConn); ok {
174 _ = tcpConn.CloseWrite()
175 }
176 errCh <- err
177 }()
178
179 go func() {
180 bufPtr := bufferPool.Get().(*[]byte)
181 defer bufferPool.Put(bufPtr)
182 _, err := io.CopyBuffer(relayConn, localConn, *bufPtr)
183 _ = relayConn.Close()
184 errCh <- err
185 }()
186
187 var firstErr error
188 for range 2 {
189 if err := <-errCh; err != nil && firstErr == nil {
190 firstErr = err
191 }
192 }
193
194 close(stopCh)
195 if errors.Is(firstErr, io.EOF) || errors.Is(firstErr, net.ErrClosed) {
196 return nil
197 }
198 return firstErr
199 }
200
201 func writeEmptyHTTPResponse(conn net.Conn) error {
202 htmlBody := `<!DOCTYPE html>
203 <html>
204 <head><title>Service Unavailable</title></head>
205 <body style="font-family:sans-serif;text-align:center;padding:50px;">
206 <h1>Service Unavailable</h1>
207 <p>The local service is not currently running.</p>
208 <p>Please start your local application and refresh this page.</p>
209 </body>
210 </html>`
211 response := fmt.Sprintf("HTTP/1.1 503 Service Unavailable\r\n"+
212 "Content-Type: text/html; charset=utf-8\r\n"+
213 "Content-Length: %d\r\n"+
214 "Connection: close\r\n"+
215 "\r\n%s", len(htmlBody), htmlBody)
216 _, err := conn.Write([]byte(response))
217 return err
218 }
219
220 // runUDPProxy waits for the exposure datagram plane and proxies it to the
221 // configured local UDP target.
222 func runUDPProxy(ctx context.Context, exposure *Exposure, udpTarget string) error {
223 udpAddrs, err := exposure.WaitDatagramReady(ctx)
224 if err != nil {
225 if ctx.Err() != nil || errors.Is(err, context.Canceled) {
226 return ctx.Err()
227 }
228 return fmt.Errorf("wait for udp readiness: %w", err)
229 }
230 if len(udpAddrs) == 0 {
231 if ctx.Err() != nil {
232 return ctx.Err()
233 }
234 return errors.New("relay did not expose any UDP listeners")
235 }
236
237 for _, udpAddr := range udpAddrs {
238 log.Info().
239 Str("udp_addr", udpAddr).
240 Msg("UDP tunnel ready")
241 }
242
243 return proxyExposureDatagrams(ctx, exposure, udpTarget)
244 }
245
246 // proxyExposureDatagrams receives datagrams from the exposure datagram plane
247 // and forwards them to the local UDP service, relaying responses back.
248 func proxyExposureDatagrams(ctx context.Context, exposure *Exposure, localAddr string) error {
249 resolvedAddr, err := net.ResolveUDPAddr("udp", localAddr)
250 if err != nil {
251 return fmt.Errorf("resolve udp addr %q: %w", localAddr, err)
252 }
253
254 mgr := newUDPFlowManager(resolvedAddr, exposure)
255 go mgr.runCleanup(ctx)
256
257 log.Info().Str("target", localAddr).Msg("udp proxy loop started, waiting for datagrams")
258 for {
259 frame, err := exposure.AcceptDatagram()
260 if err != nil {
261 if ctx.Err() != nil {
262 return ctx.Err()
263 }
264 if errors.Is(err, net.ErrClosed) {
265 break
266 }
267 return fmt.Errorf("accept datagram: %w", err)
268 }
269
270 log.Debug().
271 Uint32("flow_id", frame.FlowID).
272 Int("bytes", len(frame.Payload)).
273 Str("address", frame.Address).
274 Str("relay_url", frame.RelayURL).
275 Str("udp_addr", frame.UDPAddr).
276 Str("target", localAddr).
277 Msg("datagram received from relay, forwarding to local")
278
279 localConn, err := mgr.getOrCreate(ctx, frame)
280 if err != nil {
281 log.Warn().
282 Err(err).
283 Uint32("flow_id", frame.FlowID).
284 Str("address", frame.Address).
285 Str("relay_url", frame.RelayURL).
286 Msg("dial local udp failed")
287 continue
288 }
289
290 if _, err := localConn.Write(frame.Payload); err != nil {
291 log.Warn().
292 Err(err).
293 Uint32("flow_id", frame.FlowID).
294 Str("address", frame.Address).
295 Str("relay_url", frame.RelayURL).
296 Msg("write to local udp failed")
297 }
298 }
299
300 return nil
301 }
302
303 type udpFlowKey struct {
304 flowID uint32
305 address string
306 relayURL string
307 }
308
309 type udpFlowEntry struct {
310 conn *net.UDPConn
311 lastSeen time.Time
312 frame types.DatagramFrame
313 }
314
315 type udpFlowManager struct {
316 target *net.UDPAddr
317 exposure *Exposure
318 mu sync.Mutex
319 flows map[udpFlowKey]*udpFlowEntry
320 }
321
322 func newUDPFlowManager(target *net.UDPAddr, exposure *Exposure) *udpFlowManager {
323 return &udpFlowManager{
324 target: target,
325 exposure: exposure,
326 flows: make(map[udpFlowKey]*udpFlowEntry),
327 }
328 }
329
330 func (m *udpFlowManager) runCleanup(ctx context.Context) {
331 ticker := time.NewTicker(15 * time.Second)
332 defer ticker.Stop()
333 for {
334 select {
335 case <-ctx.Done():
336 return
337 case <-ticker.C:
338 m.mu.Lock()
339 now := time.Now()
340 for key, f := range m.flows {
341 if now.Sub(f.lastSeen) > 30*time.Second {
342 _ = f.conn.Close()
343 delete(m.flows, key)
344 }
345 }
346 m.mu.Unlock()
347 }
348 }
349 }
350
351 func (m *udpFlowManager) getOrCreate(ctx context.Context, frame types.DatagramFrame) (*net.UDPConn, error) {
352 key := udpFlowKey{
353 flowID: frame.FlowID,
354 address: frame.Address,
355 relayURL: frame.RelayURL,
356 }
357
358 m.mu.Lock()
359 if f, ok := m.flows[key]; ok {
360 f.lastSeen = time.Now()
361 m.mu.Unlock()
362 return f.conn, nil
363 }
364 m.mu.Unlock()
365
366 localConn, err := net.DialUDP("udp", nil, m.target)
367 if err != nil {
368 return nil, err
369 }
370
371 m.mu.Lock()
372 if f, ok := m.flows[key]; ok {
373 m.mu.Unlock()
374 _ = localConn.Close()
375 f.lastSeen = time.Now()
376 return f.conn, nil
377 }
378 m.flows[key] = &udpFlowEntry{
379 conn: localConn,
380 lastSeen: time.Now(),
381 frame: types.DatagramFrame{
382 FlowID: frame.FlowID,
383 Address: frame.Address,
384 RelayURL: frame.RelayURL,
385 UDPAddr: frame.UDPAddr,
386 },
387 }
388 m.mu.Unlock()
389
390 go m.readLoop(ctx, key, localConn)
391 return localConn, nil
392 }
393
394 func (m *udpFlowManager) removeFlow(key udpFlowKey) {
395 m.mu.Lock()
396 if f, ok := m.flows[key]; ok {
397 _ = f.conn.Close()
398 delete(m.flows, key)
399 }
400 m.mu.Unlock()
401 }
402
403 func (m *udpFlowManager) readLoop(ctx context.Context, key udpFlowKey, conn *net.UDPConn) {
404 buf := make([]byte, 65535)
405 for {
406 n, err := conn.Read(buf)
407 if err != nil {
408 if ctx.Err() != nil {
409 return
410 }
411 log.Debug().
412 Err(err).
413 Uint32("flow_id", key.flowID).
414 Str("address", key.address).
415 Str("relay_url", key.relayURL).
416 Msg("local read ended")
417 m.removeFlow(key)
418 return
419 }
420
421 m.mu.Lock()
422 entry := m.flows[key]
423 if entry == nil {
424 m.mu.Unlock()
425 return
426 }
427 entry.lastSeen = time.Now()
428 replyFrame := entry.frame
429 replyFrame.Payload = bytes.Clone(buf[:n])
430 m.mu.Unlock()
431
432 if sendErr := m.exposure.SendDatagram(replyFrame); sendErr != nil {
433 log.Debug().
434 Err(sendErr).
435 Uint32("flow_id", key.flowID).
436 Str("address", key.address).
437 Str("relay_url", key.relayURL).
438 Msg("send datagram to relay failed")
439 m.removeFlow(key)
440 return
441 }
442 }
443 }