Skip to content

Commit 952622a

Browse files
committed
TUN-8709: Add session migration for datagram v3
When a registration response from cloudflared gets lost on it's way back to the edge, the edge service will retry and send another registration request. Since cloudflared already has bound the local UDP socket for the provided request id, we want to re-send the registration response. There are three types of retries that the edge will send: 1. A retry from the same QUIC connection index; cloudflared will just respond back with a registration response and reset the idle timer for the session. 2. A retry from a different QUIC connection index; cloudflared will need to migrate the current session connection to this new QUIC connection and reset the idle timer for the session. 3. A retry to a different cloudflared connector; cloudflared will eventually time the session out since no further packets will arrive to the session at the original connector. Closes TUN-8709
1 parent 70393b6 commit 952622a

File tree

9 files changed

+359
-75
lines changed

9 files changed

+359
-75
lines changed

connection/quic_datagram_v3.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ type datagramV3Connection struct {
2424
func NewDatagramV3Connection(ctx context.Context,
2525
conn quic.Connection,
2626
sessionManager cfdquic.SessionManager,
27+
index uint8,
2728
logger *zerolog.Logger,
2829
) DatagramSessionHandler {
29-
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, logger)
30+
datagramMuxer := cfdquic.NewDatagramConn(conn, sessionManager, index, logger)
3031

3132
return &datagramV3Connection{
3233
conn,

quic/v3/datagram.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,8 +284,6 @@ const (
284284
ResponseDestinationUnreachable SessionRegistrationResp = 0x01
285285
// Session registration was unable to bind to a local UDP socket.
286286
ResponseUnableToBindSocket SessionRegistrationResp = 0x02
287-
// Session registration is already bound to another connection.
288-
ResponseSessionAlreadyConnected SessionRegistrationResp = 0x03
289287
// Session registration failed with an unexpected error but provided a message.
290288
ResponseErrorWithMsg SessionRegistrationResp = 0xff
291289
)

quic/v3/manager.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,19 @@ import (
1212
)
1313

1414
var (
15-
ErrSessionNotFound = errors.New("session not found")
15+
// ErrSessionNotFound indicates that a session has not been registered yet for the request id.
16+
ErrSessionNotFound = errors.New("session not found")
17+
// ErrSessionBoundToOtherConn is returned when a registration already exists for a different connection.
1618
ErrSessionBoundToOtherConn = errors.New("session is in use by another connection")
19+
// ErrSessionAlreadyRegistered is returned when a registration already exists for this connection.
20+
ErrSessionAlreadyRegistered = errors.New("session is already registered for this connection")
1721
)
1822

1923
type SessionManager interface {
2024
// RegisterSession will register a new session if it does not already exist for the request ID.
2125
// During new session creation, the session will also bind the UDP socket for the origin.
2226
// If the session exists for a different connection, it will return [ErrSessionBoundToOtherConn].
23-
RegisterSession(request *UDPSessionRegistrationDatagram, conn DatagramWriter) (Session, error)
27+
RegisterSession(request *UDPSessionRegistrationDatagram, conn DatagramConn) (Session, error)
2428
// GetSession returns an active session if available for the provided connection.
2529
// If the session does not exist, it will return [ErrSessionNotFound]. If the session exists for a different
2630
// connection, it will return [ErrSessionBoundToOtherConn].
@@ -45,12 +49,14 @@ func NewSessionManager(log *zerolog.Logger, originDialer DialUDP) SessionManager
4549
}
4650
}
4751

48-
func (s *sessionManager) RegisterSession(request *UDPSessionRegistrationDatagram, conn DatagramWriter) (Session, error) {
52+
func (s *sessionManager) RegisterSession(request *UDPSessionRegistrationDatagram, conn DatagramConn) (Session, error) {
4953
s.mutex.Lock()
5054
defer s.mutex.Unlock()
5155
// Check to make sure session doesn't already exist for requestID
52-
_, exists := s.sessions[request.RequestID]
53-
if exists {
56+
if session, exists := s.sessions[request.RequestID]; exists {
57+
if conn.ID() == session.ConnectionID() {
58+
return nil, ErrSessionAlreadyRegistered
59+
}
5460
return nil, ErrSessionBoundToOtherConn
5561
}
5662
// Attempt to bind the UDP socket for the new session

quic/v3/manager_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,14 @@ func TestRegisterSession(t *testing.T) {
3434

3535
// We shouldn't be able to register another session with the same request id
3636
_, err = manager.RegisterSession(&request, &noopEyeball{})
37+
if !errors.Is(err, v3.ErrSessionAlreadyRegistered) {
38+
t.Fatalf("session is already registered for this connection: %v", err)
39+
}
40+
41+
// We shouldn't be able to register another session with the same request id for a different connection
42+
_, err = manager.RegisterSession(&request, &noopEyeball{connID: 1})
3743
if !errors.Is(err, v3.ErrSessionBoundToOtherConn) {
38-
t.Fatalf("session should not be able to be registered again: %v", err)
44+
t.Fatalf("session is already registered for a separate connection: %v", err)
3945
}
4046

4147
// Get session

quic/v3/muxer.go

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ type DatagramConn interface {
1919
DatagramWriter
2020
// Serve provides a server interface to process and handle incoming QUIC datagrams and demux their datagram v3 payloads.
2121
Serve(context.Context) error
22+
// ID indicates connection index identifier
23+
ID() uint8
2224
}
2325

2426
// DatagramWriter provides the Muxer interface to create proper Datagrams when sending over a connection.
@@ -41,24 +43,30 @@ type QuicConnection interface {
4143

4244
type datagramConn struct {
4345
conn QuicConnection
46+
index uint8
4447
sessionManager SessionManager
4548
logger *zerolog.Logger
4649

4750
datagrams chan []byte
4851
readErrors chan error
4952
}
5053

51-
func NewDatagramConn(conn QuicConnection, sessionManager SessionManager, logger *zerolog.Logger) DatagramConn {
54+
func NewDatagramConn(conn QuicConnection, sessionManager SessionManager, index uint8, logger *zerolog.Logger) DatagramConn {
5255
log := logger.With().Uint8("datagramVersion", 3).Logger()
5356
return &datagramConn{
5457
conn: conn,
58+
index: index,
5559
sessionManager: sessionManager,
5660
logger: &log,
5761
datagrams: make(chan []byte, demuxChanCapacity),
5862
readErrors: make(chan error, 2),
5963
}
6064
}
6165

66+
func (c datagramConn) ID() uint8 {
67+
return c.index
68+
}
69+
6270
func (c *datagramConn) SendUDPSessionDatagram(datagram []byte) error {
6371
return c.conn.SendDatagram(datagram)
6472
}
@@ -163,9 +171,20 @@ func (c *datagramConn) Serve(ctx context.Context) error {
163171
// This method handles new registrations of a session and the serve loop for the session.
164172
func (c *datagramConn) handleSessionRegistrationDatagram(ctx context.Context, datagram *UDPSessionRegistrationDatagram) {
165173
session, err := c.sessionManager.RegisterSession(datagram, c)
166-
if err != nil {
174+
switch err {
175+
case nil:
176+
// Continue as normal
177+
case ErrSessionAlreadyRegistered:
178+
// Session is already registered and likely the response got lost
179+
c.handleSessionAlreadyRegistered(datagram.RequestID)
180+
return
181+
case ErrSessionBoundToOtherConn:
182+
// Session is already registered but to a different connection
183+
c.handleSessionMigration(datagram.RequestID)
184+
return
185+
default:
167186
c.logger.Err(err).Msgf("session registration failure")
168-
c.handleSessionRegistrationFailure(datagram.RequestID, err)
187+
c.handleSessionRegistrationFailure(datagram.RequestID)
169188
return
170189
}
171190
// Make sure to eventually remove the session from the session manager when the session is closed
@@ -197,17 +216,49 @@ func (c *datagramConn) handleSessionRegistrationDatagram(ctx context.Context, da
197216
c.logger.Err(err).Msgf("session was closed with an error")
198217
}
199218

200-
func (c *datagramConn) handleSessionRegistrationFailure(requestID RequestID, regErr error) {
201-
var errResp SessionRegistrationResp
202-
switch regErr {
203-
case ErrSessionBoundToOtherConn:
204-
errResp = ResponseSessionAlreadyConnected
205-
default:
206-
errResp = ResponseUnableToBindSocket
219+
func (c *datagramConn) handleSessionAlreadyRegistered(requestID RequestID) {
220+
// Send another registration response since the session is already active
221+
err := c.SendUDPSessionResponse(requestID, ResponseOk)
222+
if err != nil {
223+
c.logger.Err(err).Msgf("session registration failure: unable to send an additional session registration response")
224+
return
207225
}
208-
err := c.SendUDPSessionResponse(requestID, errResp)
226+
227+
session, err := c.sessionManager.GetSession(requestID)
228+
if err != nil {
229+
// If for some reason we can not find the session after attempting to register it, we can just return
230+
// instead of trying to reset the idle timer for it.
231+
return
232+
}
233+
// The session is already running in another routine so we want to restart the idle timeout since no proxied
234+
// packets have come down yet.
235+
session.ResetIdleTimer()
236+
}
237+
238+
func (c *datagramConn) handleSessionMigration(requestID RequestID) {
239+
// We need to migrate the currently running session to this edge connection.
240+
session, err := c.sessionManager.GetSession(requestID)
241+
if err != nil {
242+
// If for some reason we can not find the session after attempting to register it, we can just return
243+
// instead of trying to reset the idle timer for it.
244+
return
245+
}
246+
247+
// Migrate the session to use this edge connection instead of the currently running one.
248+
session.Migrate(c)
249+
250+
// Send another registration response since the session is already active
251+
err = c.SendUDPSessionResponse(requestID, ResponseOk)
252+
if err != nil {
253+
c.logger.Err(err).Msgf("session registration failure: unable to send an additional session registration response")
254+
return
255+
}
256+
}
257+
258+
func (c *datagramConn) handleSessionRegistrationFailure(requestID RequestID) {
259+
err := c.SendUDPSessionResponse(requestID, ResponseUnableToBindSocket)
209260
if err != nil {
210-
c.logger.Err(err).Msgf("unable to send session registration error response (%d)", errResp)
261+
c.logger.Err(err).Msgf("unable to send session registration error response (%d)", ResponseUnableToBindSocket)
211262
}
212263
}
213264

0 commit comments

Comments
 (0)