diff --git a/connection/quic.go b/connection/quic.go index 6b10e5d1..87c3f00a 100644 --- a/connection/quic.go +++ b/connection/quic.go @@ -168,6 +168,7 @@ func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) er return rpcStream.Serve(q, q.logger) } +// RegisterUdpSession is the RPC method invoked by edge to register and run a session func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error { // Each session is a series of datagram from an eyeball to a dstIP:dstPort. // (src port, dst IP, dst port) uniquely identifies a session, so it needs a dedicated connected socket. @@ -178,22 +179,60 @@ func (q *QUICConnection) RegisterUdpSession(ctx context.Context, sessionID uuid. } session, err := q.sessionManager.RegisterSession(ctx, sessionID, originProxy) if err != nil { - q.logger.Err(err).Msgf("Failed to register udp session %s", sessionID) + q.logger.Err(err).Str("sessionID", sessionID.String()).Msgf("Failed to register udp session") return err } - go func() { - defer q.sessionManager.UnregisterSession(q.session.Context(), sessionID) - if err := session.Serve(q.session.Context(), closeAfterIdleHint); err != nil { - q.logger.Debug().Err(err).Str("sessionID", sessionID.String()).Msg("session terminated") - } - }() + + go q.serveUDPSession(session, closeAfterIdleHint) + q.logger.Debug().Msgf("Registered session %v, %v, %v", sessionID, dstIP, dstPort) return nil } -func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error { - q.sessionManager.UnregisterSession(ctx, sessionID) - return nil +func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, closeAfterIdleHint time.Duration) { + ctx := q.session.Context() + closedByRemote, err := session.Serve(ctx, closeAfterIdleHint) + // If session is terminated by remote, then we know it has been unregistered from session manager and edge + if !closedByRemote { + if err != nil { + q.closeUDPSession(ctx, session.ID, err.Error()) + } else { + q.closeUDPSession(ctx, session.ID, "terminated without error") + } + q.logger.Debug().Err(err).Str("sessionID", session.ID.String()).Msg("session terminated") + return + } + q.logger.Debug().Err(err).Msg("Session terminated by edge") +} + +// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge +func (q *QUICConnection) closeUDPSession(ctx context.Context, sessionID uuid.UUID, message string) { + q.sessionManager.UnregisterSession(ctx, sessionID, message, false) + stream, err := q.session.OpenStream() + if err != nil { + // Log this at debug because this is not an error if session was closed due to lost connection + // with edge + q.logger.Debug().Err(err).Str("sessionID", sessionID.String()). + Msgf("Failed to open quic stream to unregister udp session with edge") + return + } + rpcClientStream, err := quicpogs.NewRPCClientStream(ctx, stream, q.logger) + if err != nil { + // Log this at debug because this is not an error if session was closed due to lost connection + // with edge + q.logger.Err(err).Str("sessionID", sessionID.String()). + Msgf("Failed to open rpc stream to unregister udp session with edge") + return + } + if err := rpcClientStream.UnregisterUdpSession(ctx, sessionID, message); err != nil { + q.logger.Err(err).Str("sessionID", sessionID.String()). + Msgf("Failed to unregister udp session with edge") + } +} + +// UnregisterUdpSession is the RPC method invoked by edge to unregister and terminate a sesssion +func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { + return q.sessionManager.UnregisterSession(ctx, sessionID, message, true) } // streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to diff --git a/connection/quic_test.go b/connection/quic_test.go index 8e53fcb2..02b616c9 100644 --- a/connection/quic_test.go +++ b/connection/quic_test.go @@ -17,29 +17,32 @@ import ( "os" "sync" "testing" + "time" "github.com/gobwas/ws/wsutil" + "github.com/google/uuid" "github.com/lucas-clemente/quic-go" "github.com/pkg/errors" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/cloudflare/cloudflared/datagramsession" quicpogs "github.com/cloudflare/cloudflared/quic" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) +var ( + testTLSServerConfig = generateTLSConfig() + testQUICConfig = &quic.Config{ + KeepAlive: true, + EnableDatagrams: true, + } +) + // TestQUICServer tests if a quic server accepts and responds to a quic client with the acceptance protocol. // It also serves as a demonstration for communication with the QUIC connection started by a cloudflared. func TestQUICServer(t *testing.T) { - quicConfig := &quic.Config{ - KeepAlive: true, - EnableDatagrams: true, - } - - // Setup test. - log := zerolog.New(os.Stdout) - // Start a UDP Listener for QUIC. udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") require.NoError(t, err) @@ -47,18 +50,6 @@ func TestQUICServer(t *testing.T) { require.NoError(t, err) defer udpListener.Close() - // Create a simple tls config. - tlsConfig := generateTLSConfig() - - // Create a client config - tlsClientConfig := &tls.Config{ - InsecureSkipVerify: true, - NextProtos: []string{"argotunnel"}, - } - - // Start a mock httpProxy - originProxy := &mockOriginProxyWithRequest{} - // This is simply a sample websocket frame message. wsBuf := &bytes.Buffer{} wsutil.WriteClientText(wsBuf, []byte("Hello")) @@ -158,25 +149,13 @@ func TestQUICServer(t *testing.T) { go func() { defer wg.Done() quicServer( - t, udpListener, tlsConfig, quicConfig, + t, udpListener, testTLSServerConfig, testQUICConfig, test.dest, test.connectionType, test.metadata, test.message, test.expectedResponse, ) }() - controlStream := fakeControlStream{} - - qC, err := NewQUICConnection( - ctx, - quicConfig, - udpListener.LocalAddr(), - tlsClientConfig, - originProxy, - &tunnelpogs.ConnectionOptions{}, - controlStream, - NewObserver(&log, &log, false), - ) - require.NoError(t, err) - go qC.Serve(ctx) + qc := testQUICConnection(ctx, udpListener.LocalAddr(), t) + go qc.Serve(ctx) wg.Wait() cancel() @@ -531,3 +510,159 @@ func (moc *mockOriginProxyWithRequest) ProxyTCP(ctx context.Context, rwa ReadWri io.Copy(rwa, rwa) return nil } + +func TestServeUDPSession(t *testing.T) { + // Start a UDP Listener for QUIC. + udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0") + require.NoError(t, err) + udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr) + require.NoError(t, err) + defer udpListener.Close() + + ctx, cancel := context.WithCancel(context.Background()) + + // Establish QUIC connection with edge + edgeQUICSessionChan := make(chan quic.Session) + go func() { + earlyListener, err := quic.Listen(udpListener, testTLSServerConfig, testQUICConfig) + require.NoError(t, err) + + edgeQUICSession, err := earlyListener.Accept(ctx) + require.NoError(t, err) + edgeQUICSessionChan <- edgeQUICSession + }() + + qc := testQUICConnection(ctx, udpListener.LocalAddr(), t) + go qc.Serve(ctx) + + edgeQUICSession := <-edgeQUICSessionChan + serveSession(ctx, qc, edgeQUICSession, closedByOrigin, io.EOF.Error(), t) + serveSession(ctx, qc, edgeQUICSession, closedByTimeout, datagramsession.SessionIdleErr(time.Millisecond*50).Error(), t) + serveSession(ctx, qc, edgeQUICSession, closedByRemote, "eyeball closed connection", t) + cancel() +} + +func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.Session, closeType closeReason, expectedReason string, t *testing.T) { + var ( + payload = []byte(t.Name()) + ) + sessionID := uuid.New() + cfdConn, originConn := net.Pipe() + // Registers and run a new session + session, err := qc.sessionManager.RegisterSession(ctx, sessionID, cfdConn) + require.NoError(t, err) + + sessionDone := make(chan struct{}) + go func() { + qc.serveUDPSession(session, time.Millisecond*50) + close(sessionDone) + }() + + // Send a message to the quic session on edge side, it should be deumx to this datagram session + muxedPayload, err := quicpogs.SuffixSessionID(sessionID, payload) + require.NoError(t, err) + err = edgeQUICSession.SendMessage(muxedPayload) + require.NoError(t, err) + + readBuffer := make([]byte, len(payload)+1) + n, err := originConn.Read(readBuffer) + require.NoError(t, err) + require.Equal(t, len(payload), n) + require.True(t, bytes.Equal(payload, readBuffer[:n])) + + // Close connection to terminate session + switch closeType { + case closedByOrigin: + originConn.Close() + case closedByRemote: + err = qc.UnregisterUdpSession(ctx, sessionID, expectedReason) + require.NoError(t, err) + case closedByTimeout: + } + + if closeType != closedByRemote { + // Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote + unregisterFromEdgeChan := make(chan struct{}) + rpcServer := &mockSessionRPCServer{ + sessionID: sessionID, + unregisterReason: expectedReason, + calledUnregisterChan: unregisterFromEdgeChan, + } + go runMockSessionRPCServer(ctx, edgeQUICSession, rpcServer, t) + + <-unregisterFromEdgeChan + } + + <-sessionDone +} + +type closeReason uint8 + +const ( + closedByOrigin closeReason = iota + closedByRemote + closedByTimeout +) + +func runMockSessionRPCServer(ctx context.Context, session quic.Session, rpcServer *mockSessionRPCServer, t *testing.T) { + stream, err := session.AcceptStream(ctx) + require.NoError(t, err) + + if stream.StreamID() == 0 { + // Skip the first stream, it's the control stream of the QUIC connection + stream, err = session.AcceptStream(ctx) + require.NoError(t, err) + } + protocol, err := quicpogs.DetermineProtocol(stream) + assert.NoError(t, err) + rpcServerStream, err := quicpogs.NewRPCServerStream(stream, protocol) + assert.NoError(t, err) + + log := zerolog.New(os.Stdout) + err = rpcServerStream.Serve(rpcServer, &log) + assert.NoError(t, err) +} + +type mockSessionRPCServer struct { + sessionID uuid.UUID + unregisterReason string + calledUnregisterChan chan struct{} +} + +func (s mockSessionRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error { + return fmt.Errorf("mockSessionRPCServer doesn't implement RegisterUdpSession") +} + +func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, reason string) error { + if s.sessionID != sessionID { + return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) + } + if s.unregisterReason != reason { + return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason) + } + close(s.calledUnregisterChan) + fmt.Println("unregister from edge") + return nil +} + +func testQUICConnection(ctx context.Context, udpListenerAddr net.Addr, t *testing.T) *QUICConnection { + tlsClientConfig := &tls.Config{ + InsecureSkipVerify: true, + NextProtos: []string{"argotunnel"}, + } + // Start a mock httpProxy + originProxy := &mockOriginProxyWithRequest{} + log := zerolog.New(os.Stdout) + qc, err := NewQUICConnection( + ctx, + testQUICConfig, + udpListenerAddr, + tlsClientConfig, + originProxy, + &tunnelpogs.ConnectionOptions{}, + fakeControlStream{}, + NewObserver(&log, &log, false), + ) + require.NoError(t, err) + return qc +} diff --git a/datagramsession/event.go b/datagramsession/event.go index 67edcd03..d79c6b31 100644 --- a/datagramsession/event.go +++ b/datagramsession/event.go @@ -1,6 +1,7 @@ package datagramsession import ( + "fmt" "io" "github.com/google/uuid" @@ -24,6 +25,22 @@ func newRegisterSessionEvent(sessionID uuid.UUID, originProxy io.ReadWriteCloser // unregisterSessionEvent is an event to stop tracking and terminate the session. type unregisterSessionEvent struct { sessionID uuid.UUID + err *errClosedSession +} + +// ClosedSessionError represent a condition that closes the session other than I/O +// I/O error is not included, because the side that closes the session is ambiguous. +type errClosedSession struct { + message string + byRemote bool +} + +func (sc *errClosedSession) Error() string { + if sc.byRemote { + return fmt.Sprintf("session closed by remote due to %s", sc.message) + } else { + return fmt.Sprintf("session closed by local due to %s", sc.message) + } } // newDatagram is an event when transport receives new datagram diff --git a/datagramsession/manager.go b/datagramsession/manager.go index 1defdd35..aa1b6644 100644 --- a/datagramsession/manager.go +++ b/datagramsession/manager.go @@ -20,7 +20,7 @@ type Manager interface { // RegisterSession starts tracking a session. Caller is responsible for starting the session RegisterSession(ctx context.Context, sessionID uuid.UUID, dstConn io.ReadWriteCloser) (*Session, error) // UnregisterSession stops tracking the session and terminates it - UnregisterSession(ctx context.Context, sessionID uuid.UUID) error + UnregisterSession(ctx context.Context, sessionID uuid.UUID, message string, byRemote bool) error } type manager struct { @@ -100,8 +100,14 @@ func (m *manager) registerSession(ctx context.Context, registration *registerSes registration.resultChan <- session } -func (m *manager) UnregisterSession(ctx context.Context, sessionID uuid.UUID) error { - event := &unregisterSessionEvent{sessionID: sessionID} +func (m *manager) UnregisterSession(ctx context.Context, sessionID uuid.UUID, message string, byRemote bool) error { + event := &unregisterSessionEvent{ + sessionID: sessionID, + err: &errClosedSession{ + message: message, + byRemote: byRemote, + }, + } select { case <-ctx.Done(): return ctx.Err() @@ -114,7 +120,7 @@ func (m *manager) unregisterSession(unregistration *unregisterSessionEvent) { session, ok := m.sessions[unregistration.sessionID] if ok { delete(m.sessions, unregistration.sessionID) - session.close() + session.close(unregistration.err) } } diff --git a/datagramsession/manager_test.go b/datagramsession/manager_test.go index 7385eaf1..e81147df 100644 --- a/datagramsession/manager_test.go +++ b/datagramsession/manager_test.go @@ -17,8 +17,9 @@ import ( func TestManagerServe(t *testing.T) { const ( - sessions = 20 - msgs = 50 + sessions = 20 + msgs = 50 + remoteUnregisterMsg = "eyeball closed connection" ) log := zerolog.Nop() transport := &mockQUICTransport{ @@ -89,7 +90,13 @@ func TestManagerServe(t *testing.T) { sessionDone := make(chan struct{}) go func() { - session.Serve(ctx, time.Minute*2) + closedByRemote, err := session.Serve(ctx, time.Minute*2) + closeSession := &errClosedSession{ + message: remoteUnregisterMsg, + byRemote: true, + } + require.Equal(t, closeSession, err) + require.True(t, closedByRemote) close(sessionDone) }() @@ -100,7 +107,7 @@ func TestManagerServe(t *testing.T) { // Make sure eyeball and origin have received all messages before unregistering the session require.NoError(t, reqErrGroup.Wait()) - require.NoError(t, mg.UnregisterSession(ctx, sessionID)) + require.NoError(t, mg.UnregisterSession(ctx, sessionID, remoteUnregisterMsg, true)) <-sessionDone return nil diff --git a/datagramsession/session.go b/datagramsession/session.go index 463c9c1c..97b44111 100644 --- a/datagramsession/session.go +++ b/datagramsession/session.go @@ -2,6 +2,7 @@ package datagramsession import ( "context" + "fmt" "io" "time" @@ -12,6 +13,10 @@ const ( defaultCloseIdleAfter = time.Second * 210 ) +func SessionIdleErr(timeout time.Duration) error { + return fmt.Errorf("session idle for %v", timeout) +} + // Each Session is a bidirectional pipe of datagrams between transport and dstConn // Currently the only implementation of transport is quic DatagramMuxer // Destination can be a connection with origin or with eyeball @@ -24,47 +29,53 @@ const ( // - Datagrams from cloudflared are read by Manager from the transport. Manager finds the corresponding Session and calls the // write method of the Session to send to eyeball type Session struct { - id uuid.UUID + ID uuid.UUID transport transport dstConn io.ReadWriteCloser // activeAtChan is used to communicate the last read/write time activeAtChan chan time.Time - doneChan chan struct{} + closeChan chan error } func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *Session { return &Session{ - id: id, + ID: id, transport: transport, dstConn: dstConn, // activeAtChan has low capacity. It can be full when there are many concurrent read/write. markActive() will // drop instead of blocking because last active time only needs to be an approximation activeAtChan: make(chan time.Time, 2), - doneChan: make(chan struct{}), + // capacity is 2 because close() and dstToTransport routine in Serve() can write to this channel + closeChan: make(chan error, 2), } } -func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) error { - serveCtx, cancel := context.WithCancel(ctx) - defer cancel() - go s.waitForCloseCondition(serveCtx, closeAfterIdle) - // QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975 - // This makes it safe to share readBuffer between iterations - readBuffer := make([]byte, s.transport.MTU()) - for { - if err := s.dstToTransport(readBuffer); err != nil { - return err +func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (closedByRemote bool, err error) { + go func() { + // QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975 + // This makes it safe to share readBuffer between iterations + readBuffer := make([]byte, s.transport.MTU()) + for { + if err := s.dstToTransport(readBuffer); err != nil { + s.closeChan <- err + return + } } + }() + err = s.waitForCloseCondition(ctx, closeAfterIdle) + if closeSession, ok := err.(*errClosedSession); ok { + closedByRemote = closeSession.byRemote } + return closedByRemote, err } -func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) { +func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time.Duration) error { + // Closing dstConn cancels read so dstToTransport routine in Serve() can return + defer s.dstConn.Close() if closeAfterIdle == 0 { // provide deafult is caller doesn't specify one closeAfterIdle = defaultCloseIdleAfter } - // Closing dstConn cancels read so Serve function can return - defer s.dstConn.Close() checkIdleFreq := closeAfterIdle / 8 checkIdleTicker := time.NewTicker(checkIdleFreq) @@ -74,14 +85,14 @@ func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time for { select { case <-ctx.Done(): - return - case <-s.doneChan: - return + return ctx.Err() + case reason := <-s.closeChan: + return reason // TODO: TUN-5423 evaluate if using atomic is more efficient case now := <-checkIdleTicker.C: // The session is considered inactive if current time is after (last active time + allowed idle time) if now.After(activeAt.Add(closeAfterIdle)) { - return + return SessionIdleErr(closeAfterIdle) } case activeAt = <-s.activeAtChan: // Update last active time } @@ -92,7 +103,7 @@ func (s *Session) dstToTransport(buffer []byte) error { n, err := s.dstConn.Read(buffer) s.markActive() if n > 0 { - if err := s.transport.SendTo(s.id, buffer[:n]); err != nil { + if err := s.transport.SendTo(s.ID, buffer[:n]); err != nil { return err } } @@ -113,6 +124,6 @@ func (s *Session) markActive() { } } -func (s *Session) close() { - close(s.doneChan) +func (s *Session) close(err *errClosedSession) { + s.closeChan <- err } diff --git a/datagramsession/session_test.go b/datagramsession/session_test.go index b55393b6..48686136 100644 --- a/datagramsession/session_test.go +++ b/datagramsession/session_test.go @@ -31,6 +31,12 @@ func TestCloseIdle(t *testing.T) { } func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.Duration) { + var ( + localCloseReason = &errClosedSession{ + message: "connection closed by origin", + byRemote: false, + } + ) sessionID := uuid.New() cfdConn, originConn := net.Pipe() payload := testPayload(sessionID) @@ -43,7 +49,18 @@ func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.D ctx, cancel := context.WithCancel(context.Background()) sessionDone := make(chan struct{}) go func() { - session.Serve(ctx, closeAfterIdle) + closedByRemote, err := session.Serve(ctx, closeAfterIdle) + switch closeBy { + case closeByContext: + require.Equal(t, context.Canceled, err) + require.False(t, closedByRemote) + case closeByCallingClose: + require.Equal(t, localCloseReason, err) + require.Equal(t, localCloseReason.byRemote, closedByRemote) + case closeByTimeout: + require.Equal(t, SessionIdleErr(closeAfterIdle), err) + require.False(t, closedByRemote) + } close(sessionDone) }() @@ -64,7 +81,7 @@ func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.D case closeByContext: cancel() case closeByCallingClose: - session.close() + session.close(localCloseReason) } <-sessionDone diff --git a/quic/quic_protocol.go b/quic/quic_protocol.go index 507d3137..bba808ee 100644 --- a/quic/quic_protocol.go +++ b/quic/quic_protocol.go @@ -251,8 +251,8 @@ func (rcs *RPCClientStream) RegisterUdpSession(ctx context.Context, sessionID uu return resp.Err } -func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error { - return rcs.client.UnregisterUdpSession(ctx, sessionID) +func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { + return rcs.client.UnregisterUdpSession(ctx, sessionID, message) } func (rcs *RPCClientStream) Close() { diff --git a/quic/quic_protocol_test.go b/quic/quic_protocol_test.go index adfdcddb..f77359e5 100644 --- a/quic/quic_protocol_test.go +++ b/quic/quic_protocol_test.go @@ -114,11 +114,13 @@ func TestRegisterUdpSession(t *testing.T) { clientStream := mockRPCStream{clientReader, clientWriter} serverStream := mockRPCStream{serverReader, serverWriter} + unregisterMessage := "closed by eyeball" rpcServer := mockRPCServer{ - sessionID: uuid.New(), - dstIP: net.IP{172, 16, 0, 1}, - dstPort: 8000, - closeIdleAfter: testCloseIdleAfterHint, + sessionID: uuid.New(), + dstIP: net.IP{172, 16, 0, 1}, + dstPort: 8000, + closeIdleAfter: testCloseIdleAfterHint, + unregisterMessage: unregisterMessage, } logger := zerolog.Nop() sessionRegisteredChan := make(chan struct{}) @@ -142,20 +144,21 @@ func TestRegisterUdpSession(t *testing.T) { // Different sessionID, the RPC server should reject the registraion assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint)) - assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID)) + assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID, unregisterMessage)) // Different sessionID, the RPC server should reject the unregistraion - assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New())) + assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New(), unregisterMessage)) rpcClientStream.Close() <-sessionRegisteredChan } type mockRPCServer struct { - sessionID uuid.UUID - dstIP net.IP - dstPort uint16 - closeIdleAfter time.Duration + sessionID uuid.UUID + dstIP net.IP + dstPort uint16 + closeIdleAfter time.Duration + unregisterMessage string } func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error { @@ -174,10 +177,13 @@ func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UU return nil } -func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error { +func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { if s.sessionID != sessionID { return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) } + if s.unregisterMessage != message { + return fmt.Errorf("expect unregister message %s, got %s", s.unregisterMessage, message) + } return nil } diff --git a/tunnelrpc/pogs/sessionrpc.go b/tunnelrpc/pogs/sessionrpc.go index 8b41b6ea..5fa1642c 100644 --- a/tunnelrpc/pogs/sessionrpc.go +++ b/tunnelrpc/pogs/sessionrpc.go @@ -15,7 +15,7 @@ import ( type SessionManager interface { RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeAfterIdleHint time.Duration) error - UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error + UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error } type SessionManager_PogsImpl struct { @@ -76,7 +76,12 @@ func (i SessionManager_PogsImpl) UnregisterUdpSession(p tunnelrpc.SessionManager return err } - return i.impl.UnregisterUdpSession(p.Ctx, sessionID) + message, err := p.Params.Message() + if err != nil { + return err + } + + return i.impl.UnregisterUdpSession(p.Ctx, sessionID, message) } type RegisterUdpSessionResponse struct { @@ -137,12 +142,15 @@ func (c SessionManager_PogsClient) RegisterUdpSession(ctx context.Context, sessi return response, nil } -func (c SessionManager_PogsClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID) error { +func (c SessionManager_PogsClient) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { client := tunnelrpc.SessionManager{Client: c.Client} promise := client.UnregisterUdpSession(ctx, func(p tunnelrpc.SessionManager_unregisterUdpSession_Params) error { if err := p.SetSessionId(sessionID[:]); err != nil { return err } + if err := p.SetMessage(message); err != nil { + return err + } return nil }) _, err := promise.Struct() diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index 2c578ff8..fd840d9c 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -150,5 +150,5 @@ struct RegisterUdpSessionResponse { interface SessionManager { # Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16, closeAfterIdleHint: Int64) -> (result :RegisterUdpSessionResponse); - unregisterUdpSession @1 (sessionId :Data) -> (); + unregisterUdpSession @1 (sessionId :Data, message: Text) -> (); } \ No newline at end of file diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index e8df05a0..89606120 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -3485,7 +3485,7 @@ func (c SessionManager) UnregisterUdpSession(ctx context.Context, params func(Se Options: capnp.NewCallOptions(opts), } if params != nil { - call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} + call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 2} call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_unregisterUdpSession_Params{Struct: s}) } } return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} @@ -3743,12 +3743,12 @@ type SessionManager_unregisterUdpSession_Params struct{ capnp.Struct } const SessionManager_unregisterUdpSession_Params_TypeID = 0x96b74375ce9b0ef6 func NewSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return SessionManager_unregisterUdpSession_Params{st}, err } func NewRootSessionManager_unregisterUdpSession_Params(s *capnp.Segment) (SessionManager_unregisterUdpSession_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) return SessionManager_unregisterUdpSession_Params{st}, err } @@ -3776,12 +3776,31 @@ func (s SessionManager_unregisterUdpSession_Params) SetSessionId(v []byte) error return s.Struct.SetData(0, v) } +func (s SessionManager_unregisterUdpSession_Params) Message() (string, error) { + p, err := s.Struct.Ptr(1) + return p.Text(), err +} + +func (s SessionManager_unregisterUdpSession_Params) HasMessage() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s SessionManager_unregisterUdpSession_Params) MessageBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err +} + +func (s SessionManager_unregisterUdpSession_Params) SetMessage(v string) error { + return s.Struct.SetText(1, v) +} + // SessionManager_unregisterUdpSession_Params_List is a list of SessionManager_unregisterUdpSession_Params. type SessionManager_unregisterUdpSession_Params_List struct{ capnp.List } // NewSessionManager_unregisterUdpSession_Params creates a new list of SessionManager_unregisterUdpSession_Params. func NewSessionManager_unregisterUdpSession_Params_List(s *capnp.Segment, sz int32) (SessionManager_unregisterUdpSession_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) return SessionManager_unregisterUdpSession_Params_List{l}, err } @@ -3861,203 +3880,204 @@ func (p SessionManager_unregisterUdpSession_Results_Promise) Struct() (SessionMa return SessionManager_unregisterUdpSession_Results{s}, err } -const schema_db8274f9144abc7e = "x\xda\xccY{p\x14e\xb6?\xa7{&\x9d@\x86" + - "IW\x0f\x04\xa6\xe4\xe6^\x0a\xcaK\x14\x14\xb8\xdeB" + - "\xae\xde\x04L\xb8&\xf2H\xcf\x90\xbb\x96\xa0eg\xe6" + - "#tv\xa6{\xe8\xee\x89\x04A\x1e\x82\x88\xe5\x0b\x04" + - "E\x94\x95\xc5r\xb7D\xdd\x85U\xd7eKke\xd7" + - "\x17\xa5\xa8X\xb8\x05\x8a\xb5\xab\xc8>(\\W\xc4\xb5" + - "\xdcu\xed\xad\xd3=\xfd\xc8$$A\xf6\x8f\xfdor" + - "\xfa\xfb\xcew\xce\xef\xfc\xbes\xcewr\xe9w*\x1b" + - "\xb9)\xd1\xda\x18\x80\xbc-Za\xb3\xfaw\x96\xef\x9c" + - "\xf0\xab\xb5 '\x11\xed[\x9eoM|e\xad}\x1f" + - "\xa2\xbc\x000mi\xc5r\x94\xd6W\x08\x00\xd2\x9a\x8a" + - "\xdf\x03\xda\xb7\x8d\xda\xf3\xc8c\xcd[n\x051\xc9\x07" + - "\x8b\x01\xa71\xa1\x15\xa5\x1e\x81V\x16\x85\x0d\xd2\xbb\xf4" + - "\xcb\xbeF\xbcda\xe2\xed7iuXu\x84T\xbf" + - " \xd4\xa3t\xd0\xd9p@ \xd5W\xe4\xdf\xda\xf5\xdf" + - "[__\x07b\x92\xeb\xa5\xfa\xd9\xca\xe5(\x1d\xa8\xa4" + - "\x95/W\xce\x07\xb4?\xdf2\xfa\x89\xef\xbf\xf9\xdaz" + - "\x10/D(Y\xfaA\xe5{\x08(}V\xf9c@" + - "\xfb\xe0\x17\x0b\xcf<\xf3\xcae\xb7\x818\x91\x16 -" + - "\xd8T5\x8e\x03\x94\x1e\xafj\x00\xb4O\x9e\xfa\xdb\x86" + - "\x9b'\xce\xbb\x17\xe4\x89\xc8\x01D9Zq\xa0*I" + - "+>\xac\"k\x1af\x1f\xdc\x97\x9cv\xff\x962\xd3" + - "\x9d\x85\xfb\x87\xd5\xa3th\x18\x19tp\xd8M\x80\xf6" + - "_F<\xf4f\xf1\xaa\xe7\xee\x0f\x9f7ex=i" + - "k\x19N\xe7\x8d\xeb\x9ep\xe3/_~\xfa\x01\x90'" + - "!\xda\xc7:.z\x97\xdf\xb1\xfb}hG\x81\x8e\x9f" + - "\x96\x1f\xbe\x8b\x8c_9\x9c\x94\xbdu\xf1\xf3?\xbf\xf7" + - "\xe9\x0d\x0f\x81|!\"\x80\x03\xd6\x87\xc3\xffJ\x0b\xbe" + - "p\x94m9\xf2\xc2\xbc\xfc\xa6\xed\xbb\\\xf7\x9d\xefc" + - "\xab9\x0e\"\xf6\xba\x96/\xf3\xed\x8f\xa6\x1f-\x01\x13" + - "\xa5Ob\xf5i\x04\x9c6\xa1\xba\x0e\x01\xed\xcb\xde;" + - "1\x7f\xeeO\x16\xff0\xb4wfl9\xed\xdd\xb0\xf8" + - "\xf4\xfe\x9aT\xfe\x892\x87\x1d_\xae\x8c\xedF\xa9=" + - "F\x0e\xcb12\xe1\xa9\x7f\xbb\xa6j\xd9\x89\xd9{@" + - "\x9c\xe4\xa9Y\x1aK\x91\x9a\xc8\xf5\xfc7\xca\xb6_<" + - "SN'\x07\xb8|\xac\x03\xa55\xa4g\xda\xca\x98c" + - "\xcf\x1d\xfb\xb7_T\xf9\xc8\xe7\xcf\xf6\x07\xf3c#:" + - "P\xda7\x82N}v\x04!3\xb2\x05\x8f\xbd8%" + - "\xf2\\8\xee#\xe3'\x09\x99\x89q\x8a\xfb\xd8Of" + - "\xc5\xb4O\xd7\xbeX\xa6\xcdYx \xde\x8a\xd2\x07q" + - "\xd2v\xd4Y\xdc\xba\xf0\xbe\xcd\xd1\x13\xf7\xbdJ\x96\x86" + - "\x08\x17%\xa2M+\xd6\x18(m\xac\xa1\x9f\xebkj" + - "y@;\xb9\xe7\x7f~4+{\xf4\xf5~,\x95\xa2" + - "\x89\xd3\x92\x98\xa0_\xb1\x04\x19z|\xd2\xde\x9b\xffx" + - "\xd7\xa1\xc3%C\x1d\x0c\xd5\x84\x13\xc2\x95\x09\xc2\xcfg" + - "@\x19J\xce\xca\x1d\x89.\x94\xf6:\xea\x9erVs" + - "'\x941\xab\x7f\xfd\xbf\xc7BA\xdb\x9b\xf8\x08!b" + - "\xcf\xfb\xff\x85]U+\x8f\x1f\x0f\x1f\xf4X\xc2Ad" + - "\x9f\xb3\xf5O?8y\xcf\xa9|\xf6w\x0e\xf1<\xcc" + - "\x8e&f\x105?K\x10\xd1k\xebb\xcd\xe3\x8e\xb4" + - "\x9dtC\xe9\xaa88r\x16-81\x92T\\v" + - "\xe3L\xb6h\xfa\xb5'\xfb\\\xf9\xe8\xa8\x19(\x8d\x1c" + - "\xe5\x90l\xd4\x06\x94&\xd6\xd6\x02\xd8\xdd?\xddt\xed" + - "\x13/\xcd;\xed\xde\x05\xc7\xd81\xb5S\x89\x1aw\xdf" + - "\xd24\xff\xf2q\xfbO\x87\x8d\x15k\x89\x9d\xd2\x84Z" + - ":i\xf1\xf4S\xff7\xe1\xeeWN\xf7G\xc1\xe6\xda" + - "z\x94\xdak\x1d\x0a\xd2\xe2Og\x7f\xefp2\x9e<" + - "S\x06`\x85\x13\xbc\xda.\x946\xd6:\xc1\xab}\x95" + - "hv\xdb\xfb7,{\xe7\xd6\xcf\xbf(\x8f\xb5\xa3\xba" + - "gL\x0a\xa5\xbb\xc6\x90\xea\x8dc\x88\x19\x0f,\xf8\xc3" + - "\xaaS[G}\xd9\xc7\xe3\x89\xc9.\x94\xaeL\xd2\xca" + - "\xcb\x93\x1b\xa4\x07\xe9\x97\xfd\xb6\xf0\xe8\x94\xa6U\xaf\x7f" + - "\x15\xba\x0bk\x92\xad\xe4\xf0\xfd\xc2\xc3\xc7W\xff\xe6\x86" + - "\xaf\xc3\x0e\xafL~D\x0eoJ\x92\xc3+>}\xf0" + - "\xea{\x16=\xf9M8\xb0\xc9\xb5\xb4\xd5*j\x1a\xcb" + - "\x19\x85H\xe6\x12\xefgfrF)h\x85\x193\x8b" + - "\xd6\x12\xa6YjF\xb1X\x8a5\x98\x05]3Y\x1b" + - "\xa2\\\xc3G\x00\"\x08 *]\x00\xf2\x8d<\xca9" + - "\x0eE\xc4\x04\x85^TI\xb8\x84G\xd9\xe2P\xe4\xb8" + - "\x04e\x1eq\xe98\x009\xc7\xa3\xbc\x8cC\xe4\x13\xc8" + - "\x03\x88\xc5\xcd\x00\xf22\x1e\xe5u\x1c\xda\x05f\xe4\x15" + - "\x8di\x10\xb7\x9a\x0d\x03\xab\x81\xc3j@\xdb`\x96\xd1" + - "\xa3t\xe4 \xceBb\xa1\xeb&\x0bc\xc0a\x0c\xd0" + - "^\xa2\x17\x0d\xb3]\xb3P\xcd\xa5\xd8b\x83\x99\xb8\x04" + - "+\x80\xc3\x0a\xc0\x81\xdcK3\xd3Tum\xae\xa2)" + - "\x9d\xcc\x00 \xcf*\xf9(\x80\x9f\xb4\xd1K\xef\xe2\x94" + - "\xed\xc0\x89\x93\x04\x0c20z\xf4\x13\xffc7p\xe2" + - "X\xc16X\xa7jZ\xcc\xc0\xf6l\xc1\xd1\xcd\xebZ" + - "#\xdaE\xcd\xfd\x80\xccp?\xc4\xe9\xd4Fl\xc3\xc0" + - ":\xbe\xafuW\xe5T\xa6Y\xf1\x16m\xb1^\x06y" + - "k\x7f\x90\xb7\x96 _\x17\x82|\xcd,\x00y\x05\x8f" + - "\xf2\xed\x1c\x8a|\x09\xf3\xf5\xf5\x00\xf2j\x1e\xe5;9" + - "\xb43\xce!-Y\x00\xf0\xd1\\\xcc\x14\xabh0\x93" + - "d#\x00\xdbxt@\x1f\x01\xb8\xaa\x9b\x19d\xbb\x17" + - "\x84\xb8bd\x96\xf8\x81\x1a\x00\xe9\xe6e\xaai\xa9Z" + - "\xe7\x02G\xde\xd0\xa6\xe7\xd4L\x0fyU\xed\xd89v" + - "\x06\x00\xa28\xf2:\x00\xe4Dq\x16@\x83\xda\xa9\xe9" + - "\x06\xb3\xb3\xaa\x99\xd15\x8d\x01\x9f\xb1Vu(9E" + - "\xcb0\xff\xa0\x8a\xbe\x07\xb9\x07\xa4\x99\xd1\xcd\x8c\xc9J" + - "\x88\xbe\xe3\xdb\x14C\xe1\xf3\xa6\\\xed\xe3\xd8|\x1d\x80" + - "\xdc\xc4\xa3\xdc\x16\xc2q.\xe18\x87G\xf9\xda\x10\x8e" + - "\xed\x84c\x1b\x8f\xf2\"\x0em\xddP;U\xed*\x06" + - "\xbc\x11f\xa0iiJ\x9e\x11f%\xcd \x9e1X@\x87\xa1C\xed%" + - "\x872\xb0\xe3\x86\x92\xef\x15a\x02\xbb\x9aGyt\xff" + - "p\xf9'F\xfbI$t\x812\xc4\xc5\x14sS\xf8" + - "d\x83\x99B1g\x91\xff\xd5\xb6\xed\x02@\x91\x1b\xcf" + - "\xa3|)\x871\xfc\xc6v\x11\x98\xb49@\xa0\x8e\x19" + - "\x86n`MP\xe2J\xc4\xca\x94\x0e@]kb\x96" + - "\xa2\xe6\x90H\xef\xf7[e\xf4\x1b\xec\xd6\x06\x88\xb8\xe2" + - "\xf1\x0d\xc4\xbd\xdeh\x10yjx\x94/\xe0\xd0\xee4" + - "\x94\x0ckc\x06\xaazv\x9e\xa2\xe9i\x9ee\xfaP" + - "a\xc4\xb9\x1e\x9arn\x91\x09\xfe\xae\x81\xf7\x1b\xac\x04" + - "Bi{[\x9dks\xc2\xb7y\xe5\xb8\xa0\xd4\xf9\x04" + - "[\xd3\x11\xe4b?\xdbl$*\xde\xce\xa3\xbc%\x94" + - "\xb57Q^\xba\x97G\xf9a\x0e\xc5H$\x81\x11\x00" + - "\xf1A\xbaY[x\x94wr\xbd\x0b\"\xebf\x9a\xd5" + - "\xa4v\x82\xc0\xcc@J&6\xa9\x9d\x0cx\xf3|3" + - "W\xe5 x\xe8\x1d\xa6\x9ec\x16kb\x99\x9cb(" + - "\x96\xda\xcd\xdc\xef%2zA\x1d\x88\xb7\xa9>\x17\x83" + - "\xf8\x1b\xf7z\x90\x10\x1d\xc6\x05\xe9O`\xa1\xd6a\x00" + - "k]\xe5d\x99\xae\xf5\xe1@pcJ<@s\xa0" + - "\xea\x16,\x9f_\xb0TA\xd7L\xb2/\x14\xfa\x19\xfd" + - "\x85\xde\x08B\xefe\xca\x8dk\xc3\x91/e\xcaM\xdb" + - "\x83 \x8b\x11\xce\x8d\xfc\x8e]\x00\xf2N\x1e\xe5'9" + - "lp\x8b8\xd6\x04\x8f\xe0R\xb4\xdcR5G\x87\xba" + - "\x8c\x92\x0b\xb2\xa9m\xb0BN\xc9\xb0f,\x95e@" + - "\x04\x0e\xd1\xa1H\xbe`0\xd3DU\xd7\xe4\xa2\x92S" + - "y\xab\xc7o\xa5\xb4b\xbe\xcd`\xdd*\xeaEs\xa6" + - "e\xb1\xbcP\xb0\xcc\xa14Z\x01@\x94\x1f\x045g" + - "\x96%\xdf\xfa \xf7\xf8\x00M\xa2\xe4{1\x8f\xf2t" + - "\x0e\xe3\xc5\xa2\x1a\xe4\xba\x9c\x9eq\xe2\x06\xf1yJ\x9e" + - "\xf5\x89v\xc5\xa0w\xb5\xd7M\xf7\x92\xed\xbfRc0" + - "p/N\xae;\xcdj\xc8d\xba\x02\x8d<\xcasB" + - "&\xb7L\x0d\xf9\xe1\x99<\xb7#\xf0C\xf8.\xeb\xf1" + - "\xac\xaacy\xca\xdc\x1e\x98%gf\x82pM\xb0f" + - " \xfb\xc2\x17j~\xa1\xce\xf1\x90l\x9c\xee\xd9(\xf5" + - "`+@z\x19\xf2\x98^\x87\x81\x99\xd2\x1a\x9c\x05\x90" + - "^A\xf2\xdb1\xb0TZ\x8fI\x80\xf4j\x92\xdf\x89" + - "\xfe\x9bA\xda\x88\xbb\x01\xd2w\x92x\x1b-\x8f\xf0\xce" + - "\x95\x90\xb6:\xea\xb7\x90|'\xc9\xa3\x91\x04F\x01\xa4" + - "\x1dX\x0f\x90\xdeF\xf2gH^\xc1%\xb0\x02@\xda" + - "\x8b]\x00\xe9=$\x7f\x9e\xe4B4A\xcf&i\x1f" + - "\x1a\x00\xe9\x9f\x91\xfc%\x92W\x8eN`%\x80\xb4\xdf" + - "\x91\xbfH\xf27H^5&\x81U\x00\xd2\x01\\\x0b" + - "\x90~\x8d\xe4\x87I>\x0c\x138\x0c@:\x84\xdb\x01" + - "\xd2\x87I\xfe[\x92\x0f\xafH\xe0p\x00\xe9\x03\xc7\x9e" + - "#$\xff\x98\xe4\xd5\x91\x04V\x03H\x1f\xe2.\x80\xf4" + - "\xc7$\xff3\xc9cB\x02c\x00\xd2'\x8e_\xa7H" + - "^\xc9\x95\xb5\xec\x1e\xa3\xca\xfar^7\xfd\x90\xb1\xd2" + - "\x1dG\x97\xeemz\x9czo\x8c\x07C0@\x8c\x03" + - "\xda\x05]\xcf\xcd\xeb\xcd\xd4\xb8\xa5t\x9a\xde\x1b\xa0&" + - "\x98K\x00\x92\xd0\xaf\xfb\x10\xd7\xb5\x96\xac\x9f\x08\xca\xb3" + - "\x8eg\x89j\xce,Zz\xb1\x00uY\xc5bY?" + - "\xe7\x18Em\xb6\xa1\xe7\x17 3\xf2\xaa\xa6\xe4\x06\xc9" + - "FU\xc0a\x15\x94R\x82\xa7{\xe0\xd4t\xf6\x17\x8d" + - "\xcfh\xae\x9c\xd1u\x85\x19\x0b\x94\xce\xa1\xe4\xa9\xa9A" + - "\xe7\x18\xd7B\x09\xa9\xae[\xc9\x15\xbfMz\xea\xddJ" + - "\xa4\x1a\xdcVd\xb0~\xdf\x1bS\x0c\x9eJz7\x84" + - "\xbd\x0b*\x86&\x88t\x0eW\xd2?d\xf3;\x99\xe5" + - "\xfe\xa2\x87+=\x1b\x84p\x99?\xb7\xdd)f\xc6\x87" + - "\xe2z0\xce\x19\xfc\xa9\xd3O\xe1\xef\xa7\xec{=g" + - "\xe8\xb9C\xb1_\xc4\xa3\xbc$\x14{\xd6\xda\xcfs'" + - "\x15\xcc9D\x9e+\x0d:\xa8P\x14x\x94Wp\x18" + - "\xa7w)\xd6\x04s\xdf^F\xf7~\x8b\x13\x15Z\xb4" + - ",\x03\\\xe6\xb19T>\xfc\x09\xe8\xe0\xdd\xd9\xd0\xdc" + - "\xf6\xba\xdeA\x01\xf7\xa7\x8ae'\x9f\xf5\xc9\xd5\xe0\x1e" + - "J<\x1b\xed\x8cX\xbc\x09+z\xb3:q\xefr\xe0" + - "\xc4\xc7\x05\x0c\xa6\x90\xe8\x0d\x1d\xc5\x1d\x06p\xe2V\x01" + - "9\x7ff\x8d\xdelZ\xdcx\x07p\xe2z\x01y\x7f" + - "\xe4\x8c\xde\xb4kJ\xcf0\x04N\\)`\xc4\x1f\xe5" + - "\xa37+\x13\x97v\x01'\xaa\x02F\xfdi6z\xe3" + - "T\xf1\xfa\xb5\xc0\x89\xed\xc1L\x07\x1a\\?\x1a\xd1\xf6" + - "8\x0au\x0eK{Ox\xdcU\x00\x8dh{=0" + - "\x7f\xb6&\xd8Y\xe5\x0d) \x9eQ,\xd6H\xcd\x99" + - "{\xff\xb1\x94\x00\xa0\x11\xe5\x08\x86F\x85\x00\xe7\xfb\xbe" + - "L\xb1:'\xce\xdf\xb6e\xf2\xf6\x7f\xcb\x94\xc4\xf7g" + - "5\x9d\xe3\x0f\xbbBz\xbbB\x0f\xdfA\x1a\xbf\xc8\xd9" + - "\xbc\xf0\xc8\x1f\xa7\xcd\xa4\xff\xdf}\xfd\x87\xa8qz\x83" + - "G\xf9H\xe8Z\xbfK\xc2\xb7y\x94\x8f\x85\x1a\xa7\xa3" + - "t\xd7\x8f\xf0(\x9f\x09\xe6\x97\x9f\xdd\x01 \x9f\xe11" + - "\x15jD\xc4\xbf\xd3\xc2\xaf\xa9\\;m\x08\xbamH" + - "\x147\x03\xa4+\xa9\x8c'\x9c6$\xe2\xb6!\"v" + - "\x00\xa4kH~A\xb8\x0d\x19\x83\xd7\x01\xa4G\x93|" + - "<\xf6~\xd7\x08E#h\xd4rz\xe7\x1cU\xeb\xb7" + - "\xb6y\x03U\xb4f+j\xaeh0\x08Jk)\xd9" + - "4\x85\xaa\xbd;iu\x87*i\"a\x16M\x7f\xe0" + - "r\x0e/\xca!U\x9ef\xc3\xd0\xd1(kb\xa7\x06" + - "M\xac\xdf\xc3R/~5\x8f\xf2\x02\x0aE\xa3\x1b\x0a" + - "\xb9#h\xbb\xeb2J\xd1d}|\x00\x9e\x19\xfe\x14" + - "\xc0\\\xa2\x17s\xd9\x14\x03\xc12z\xca \x18\xb4\x99" + - "M\xb3\xb8\x97\xb9\xdc\xe1\xb0\xf7\x8f\x0e\xf4\xfe\x9f\x11\x1a" + - "\x0e{\x13z\xf4\xfem\xd5w8\xeca\xd0g8\xec" + - "~p8\xda{8|\x1e\xcfW\xb7\x8c\x852\xc69" + - "\xcdL\x87\xd23p\xe5\x09Zvf\xde" + + "\x84\xc9\xcdt\x0f\xdd=\x91 \xc8\x87 b\xf9\x05\x82" + + "\"\xca\xc9ayW\xa0\xde\xc1\xa9\xe7\xb2%\xb5\xb2+" + + "*\xa5\xa8X\xb0\x85\x8a\xb5\x8b\xc8\xeeJ\xc1\xba\"\xac" + + "\xe5\xaeko=\xdd\xd3\x1f\x99\x84$\xc8\xfe\xb1\xffM" + + "\x9e~\xde\xf7}>~\xcf\xef}\xde'\xd7wT6" + + "r\xf5\xe1\x9a\x08\x80\xbc%\\a\xb1\xba\x0f\x97n\xbf" + + "\xeag\xabAN Z\xf7\xbc\xd6\x1a\xff\xd6\\\xfd\x09" + + "\x84y\x01`\xca\xe2\x8a\xa5(\xad\xad\x10\x00\xa4U\x15" + + "\xbf\x06\xb4\xee\x1b\xb5\xfb\x99\xe7fl\xba\x17\xc4\x04\xef" + + "+\x03NaB+J=\x02i\x16\x85u\xd2Q\xfa" + + "e\xdd\"^\xb7 \xfe\xc1{\xa4\x1d\xdc:D[\xef" + + "\x13\xeaP:d/8(\xd0\xd67\xe6\xdf\xdf\xf1\x0f" + + "\x9b\xdfY\x03b\x82\xeb\xb5\xf5+\x95KQ:XI" + + "\x9a\x07*\xe7\x02Z_o\x1a\xfd\xfc\x7f\xbe\xf7\xf6Z" + + "\x10\xafF(Y\xfai\xe5\xc7\x08(}U\xf9\xbf\x80" + + "\xd6\xa1\x0b\x0b\xce\xbf\xfc\xe6\x0d\xf7\x818\x81\x14\x90\x14" + + "6T\x8d\xe3\x00\xa5\x9dU\x0d\x80\xd6\xe93\x7f\\w" + + "\xf7\x849\x8f\x82<\x019\x800G\x1a\x07\xab\x12\xa4" + + "q\xa2\x8a\xaci\x98yhob\xca\xe3\x9b\xcaL\xb7" + + "\x15\xf7\x0f\xabC\xe9\xf002\xe8\xd0\xb0\xbb\x00\xad\xdf" + + "\x8fx\xea\xbd\xe2M\xaf>^:\xcfV\xaa\x1f^G" + + "\xbb\xb5\x0c'\x85q\xddW\xdd\xf9\xd3\x03/=\x01\xf2" + + "DD\xebx\xfb5G\xf9m\xbb>\x81\xf9(\xd0\xf1" + + "Sv\x0e\xdfA\xc6\xef\xb5u\xdf\xbf\xf6\xb5\x1f?\xfa" + + "\xd2\xba\xa7@\xbe\x1a\x11\xc0\x0e\xd6\xd8\xea?\x90B}" + + "5\x19\xbf\xe9\xd8\xbe9\xf9\x0d[w8\xee\xdb\xdf\xff" + + "\xad\x9a\xe3 d\xadi\xf9&?\xff\xd9\xd4\xb3\xa5\xc0" + + "\x84\xe9\xd3\xec\xeas\x088E\xa9\xaeE@\xeb\x86\x8f" + + "O\xcd\x9d\xfd\x7f\x1d\xff\x1dX\xbb<\xb2\x94\xd6\xae\xeb" + + "8\xb7?\x96\xcc?_\xe6\xb0\x1d\xbb\x9e\xc8.\x946" + + "D\xc8\xe1\x87\"d\xc2\x8b\x7fsK\xd5\x92S3w" + + "\x838\xd1\xdd\xe6\xc5H\x92\xb6\x09\xdd\xce\x7f\xafl\xf9" + + "\xc9\xcb\xe5p\xb2c\xb23\xd2\x8e\xd2>\xdag\xca\xde" + + "\x88m\xcf\x03\xfb\xb7^S\xf9\xcc\xd7\xaf\xf4\x17\xe6\x13" + + "#\xdaQ\xba0\x82N\xfdj\x04Efd\x0b\x1e\x7f" + + "\xbd>\xf4j0\xefr\xf44E\x86E)\xefc\xcf" + + "N\x8f\xa8_\xae~\xbdl7[1\x1ckEiL" + + "\x8cv\x1b\x19#\xe5\xd6\x05\x8fm\x0c\x9fz\xec-\xb2" + + "4\x00\xb80\x01m\xca\x9e\x98\x8e\xd2\x81\x98\x9d\xedX" + + "\x0d\x0fh%v\xff\xd3\xffL\xcf|\xf4N?\x96J" + + "M\xf1s\xd2\xec8\xfdj\x89\x93\xa1''\xee\xb9\xfb" + + "\x8b\x87\x0e\x1f)\x19j\xc7\xf0\xb9\xb8\x9d\xc2\xbdq\x8a" + + "\x9f\x87\x80\xb2(\xd9\x9a\x1f\xc5\xbbP:ko\xf7\x85" + + "\xad\xcd\x9dR\xc6\xac\xfc\xf9?\x1f\x0f$\xedl\xfc3" + + "\x84\x905\xe7_\x17tU-?y2x\xd0\x89\xb8" + + "\x1d\x91\x0b\xf6\xd2\xdf\xfe\xd7\xe9G\xce\xe43\xbf\xb2\x81" + + "\xe7\xc6l\xe4\xc8i\x04\xcd\x89#\x09\xe85\xb5\x91\x19" + + "\xe3\x8e\xb5\x9dvR\xe9lQ5j:)\\9\x8a" + + "\xb6\xb8\xe1\xce&\xb6p\xea\xad\xa7\xfb\x94|\xd3\xa8i" + + "(\xc9\xa3l\x90\x8dZ\x87\x12\xab\xa9\x01\xb0\xba\xff\x7f" + + "\xc3\xad\xcf\xbf1\xe7\x9cS\x0b\xb6\xb1\xf3k&\x134" + + "\x1e\xbe\xa7y\xee?\x8e\xdb\x7f.h\xec\xec\x1aB\xa7" + + "\xa4\xd4\xd0I\x1dS\xcf\xfc\xcbU\x0f\xbfy\xae?\x08" + + "\xae\xaa\xa9CiC\x8d\x0dAR\xfer\xe6\x7f\x1cI" + + "D\x13\xe7\xcb\x02Xa'\xaf\xa6\x0b\xa5\x035v\xf2" + + "j\xde\"\x98\xdd\xf7\xc9\x1dK>\xbc\xf7\xeb\x0b\xe5\xb9" + + "\xb6\xb7~eL\x12\xa5\x83cl~\x19C\xc8xb" + + "\xdeoV\x9c\xd9<\xea\x9b\xbe$\x97\xe8B\xa9'a" + + "\x93\\b\x9dt\x94~Y\x1f\x08\xcf\xd67\xafx\xe7" + + "\xdb@-\xecK\xb4\x92\xc3\x8f\x0bO\x9f\\\xf9\x8b;" + + "\xbe\x0b:\xbc7\xf1\x199|(A\x0e/\xfb\xf2\xc9" + + "\x9b\x1fY\xf8\xc2\xf7\xc1\xc4&V\xd3R\xb3\xa8\xaa," + + "\xa7\x17B\xe9\xeb\xdc\x9f\xe9Ii\xa5\xa0\x16\xa65\x15" + + "\xcdEL5\xb3i\xc5dI\xd6`\x144\xd5`m" + + "\x88r\x8c\x0f\x01\x84\x10@T\xba\x00\xe4;y\x94s" + + "\x1c\x8a\x88qJ\xbd\x98%\xe1\"\x1ee\x93C\x91\xe3" + + "\xe2\xc4<\xe2\xe2q\x00r\x8eGy\x09\x87\xc8\xc7\x91" + + "\x07\x10\x8b\x1b\x01\xe4%<\xcak8\xb4\x0aL\xcf+" + + "*S!j\xce\xd0u\xac\x06\x0e\xab\x01-\x9d\x99z" + + "\x8f\xd2\x9e\x83(\x0b\x88\x85\xae\xbbL\x8c\x00\x87\x11@" + + "k\x91V\xd4\x8d\xf9\xaa\x89\xd9\\\x92u\xe8\xcc\xc0E" + + "X\x01\x1cV\x00\x0e\xe4^\x8a\x19FVSg+\xaa" + + "\xd2\xc9t\x00\xf2\xac\x92\x0f\x03x\xa4\x8d.\xbd\x8b\xf5" + + "[\x81\x13'\x0a\xe830\xba\xf0\x13\xaf\xdc\x05\x9c8" + + "V\xb0t\xd6\x995L\xa6\xe3\xfcL\xc1\xde\x9b\xd7\xd4" + + "F\xb4\x8a\xaa\xf3\x01\x99\xee|\x88\xd2\xa9\x8d\xd8\x86\xbe" + + "u|_\xebn\xcae\x99jF[\xd4\x0e\xad,\xe4" + + "\xad\xfd\x85\xbc\xb5\x14\xf25\x81\x90\xaf\x9a\x0e /\xe3" + + "Q\xbe\x9fC\x91/\xc5|m\x1d\x80\xbc\x92G\xf9A" + + "\x0e\xad\xb4}HK\x06\x00\xbchv0\xc5,\xea\xcc" + + " \xd9\x08\xc06\x1e\xed\xa0\x8f\x00\\\xd1\xcdt\xb2\xdd" + + "MBT\xd1\xd3\x8b\xbcD\x0d\x10\xe9\x19K\xb2\x86\x99" + + "U;\xe7\xd9\xf2\x866-\x97M\xf7\x90W\xd5\xb6\x9d" + + "c\xa7\x01 \x8a#o\x03@N\x14\xa7\x034d;" + + "UMgV&k\xa45Ue\xc0\xa7\xcd\x15\xedJ" + + "NQ\xd3\xcc;\xa8\xa2\xefA\xce\x01)\xa6w3}" + + "\x92\x12\x80\xef\xf86EW\xf8\xbc!W{q\x9cq" + + "\x1b\x80\xdc\xcc\xa3\xdc\x16\x88\xe3l\x8a\xe3,\x1e\xe5[" + + "\x03q\x9cOql\xe3Q^\xc8\xa1\xa5\xe9\xd9\xce\xac" + + "z\x13\x03^\x0f\"\xd00U%\xcf(f\xa5x\xac" + + "\xd0\x0afVS\x0d\x8c\xf9\xfc\x0f\x88\xb1@\xa4\x84\xc1" + + "09\xc9\x85\x94\x8b(M\x1d\x9fdFQ\xc8\x99\x86" + + "\x1c\xf2<\x89L\x03\x90+y\x94\xe3\x1c6\xe8\xcc(" + + "\xe6L\x8c\xf9\xd7\xec_\xe2T7|\x01\x18&\xfb\x83" + + "\xe1d\x009\xc3\xa3\\\xe0\x10K\xd1\xcbO\x0f\xb0\x01" + + "\x8f\x0e\x0a\x17o\x05\x90M\x1e\xe5\x95\x1cZ\x86sH" + + "\x0b`\xc6\x8dhm\xc60[\x0a\xee_+2\x86\xd9" + + "\xa6\xe9&\x0a\xc0\xa1\x00\x84[\xcd`M\x1dTS-" + + "\x99\x1c\xbb9\xcb\xab&\x86\x81\xc30\x0cXT\x0e>" + + "\xa2DlN\xb5\xbb\xdeL 0\xfc\x1d\x8f\xf2\xdf\x07" + + "\xbc\xa9'\x1e\xbb\x9eG\xf9F\x0e-%\x9d\xd6\x8a\xaa" + + "9\x0fx\xa5\xb3\x0c\xf3)\x06\xd1\xb4\xce|8\x0c=" + + "\xd4.9\x94\x05;\xaa+y#h^\xb2?\xf3(" + + "\xb0\xd7\xf2(O\xed?\x86+\xf2\xcc0\x94N\xd6\xa7" + + "B\xc3\xfd\xb0\x0dUY\x9a\x00\x9bd\x0e\xcfO\xd2\x99" + + "!\x14s&YQmY\x8e\x19\x94\xde\xf1<\xca\xd7" + + "s\x18\xc1\xef-\xc7\x8e\x89\x1b\xfd0\xd52]\xd7t" + + "\x8c\xf9\xf7`\x09}\xe9\xd2\x01\xa8\xa9\xcd\xccT\xb29" + + "\xa4\xca\xf0\x9a\xb22\x8c\x0eV\xda~\xd8\x1c\xf1\xf8\x06" + + "\x02h\xbeWQ\x10\xc2b<\xcaWphu\xeaJ" + + "\x9a\xb51\x1d\xb3Zf\x8e\xa2j)\x9e\xa5\xfb\xe0e" + + "\xc4\xa5\x1e\x9a\xb4K\xcd\x00o\xd5\xc0\xebuV\x0aB" + + "iy[\xadcs\xdc\xb3y\xf98\xff>\xf4\xd2\xbc" + + "\xaa\xdd'l\x8f\x92\xd6\x13^\xef\xe7Q\xde\x14\xa0\xf6" + + "\x0dD^\x8f\xf2(?\xcd\xa1\x18\x0a\xc51\x04 >" + + "I(\xd9\xc4\xa3\xbc\x9d\xeb}k\xb2n\xa6\x9a\xcd\xd9" + + "N\x10\x98\xe1K\xc9\xc4\xe6l'\x03\xde\xb8\\z\xab" + + "\x1c$\x1eZ\xbb\xa1\xe5\x98\xc9\x9aY:\xa7\xe8\x8a\x99" + + "\xedf\xce\xf7\x12\x18\xdd\xa4\x0e\x84\xdbd\x9f\xea!\xfc" + + "F\xddF%\x00\x87q>G\x0a,\xd0_\x0c`\xad" + + "\xb39Y\xa6\xa9}0\xe0WL\x09\x07h\x0ct\x05" + + "\xfa\xeas\x0bfV\xd0T\x83\xec\x0b\xa4~Z\x7f\xa9" + + "\xd7\xfd\xd4\xbbt\xba~u0\xf3%:\xdd\xb0\xd5O" + + "\xb2\x18\xe2\x9c\xcco\xdb\x01 o\xe7Q~\x81\xc3\x06" + + "\xe7\xa6\xc7\x98\xffR.e\xcb\xb9\xcffiP\x9bV" + + "r>\xe5Z:+\xe4\x944\x9b\x81\xa5\xbb\x1b\x10\x81" + + "C\xb4!\x92/\xe8\xcc00\xab\xa9rQ\xc9ey" + + "\xb3\xc7\xeb\xb7\xd4b\xbeMg\xddY\xd4\x8aF\x93i" + + "\xb2\xbcP0\x8d\xa1tc~\x80\x88\x1f\x84l\xce(" + + "c\xe8:\x9f{\xbc\x00M\xec\xf2)0Z,f=" + + "\xee\xb3rZ\xda\xce\x1bD\xe7(\xf9\xbe\x14X1h" + + "\xad\xf6\xaat\x97\x91\xff\x9a\xba\x87\x81\x1bvr\xdd\xee" + + "h\x03&S\x094\xf2(\xcf\x0a\x98\xdc29\xe0\x87" + + "k\xf2\xecv\xdf\x0f\xe1\xdfY\x8fkU-\xcb\x13s" + + "\xbb\xc1,9\xd3\x04\xc2-\xbe\xce@\xf6\x05\x0bjn" + + "\xa1\xd6\xf6\x90l\x9c\xea\xda(\xf5`+@j\x09\xf2" + + "\x98Z\x83\xbe\x99\xd2*\x9c\x0e\x90ZF\xf2\xfb\xd1\xb7" + + "TZ\x8b\x09\x80\xd4J\x92?\x88\xde\xc3BZ\x8f\xbb" + + "\x00R\x0f\x92x\x0b\xa9\x87x\xbb$\xa4\xcd\xf6\xf6\x9b" + + "H\xbe\x9d\xe4\xe1P\x1c\xc3\x00\xd26\xac\x03Hm!" + + "\xf9\xcb$\xaf\xe0\xe2X\x01 \xed\xc1.\x80\xd4n\x92" + + "\xbfFr!\x1c\xa7\xb7\x95\xb4\x17u\x80\xd4\x8fH\xfe" + + "\x06\xc9+G\xc7\xb1\x12@\xdao\xcb_'\xf9\xbb$" + + "\xaf\x1a\x13\xc7*\x00\xe9 \xae\x06H\xbdM\xf2#$" + + "\x1f\x86q\x1c\x06 \x1d\xc6\xad\x00\xa9#$\xff%\xc9" + + "\x87W\xc4q8\x80\xf4\xa9m\xcf1\x92\x7fN\xf2\xea" + + "P\x1c\xab\x01\xa4\x13\xb8\x03 \xf59\xc9\x7fG\xf2\x88" + + "\x10\xc7\x08\x80t\xd6\xf6\xeb\x0c\xc9+\xb9\xb2\xbe\xdeE" + + "TY\xf3\xcek\x86\x972V\xaaqt\xe0\xde\xa6E" + + "\xa9A\xc7\xa8?)\x03\xc4(\xa0U\xd0\xb4\xdc\x9c\xde" + + "H\x8d\x9aJ\xa7\xe1>\x14b\xfe\xf0\x02\x90\x84\xde\xbd" + + "\x0fQMm\xc9xDP\xce:\xae%Y\xa3\xa9h" + + "j\xc5\x02\xd4f\x14\x93e<\xce\xd1\x8b\xeaL]\xcb" + + "\xcfC\xa6\xe7\xb3\xaa\x92\x1b\x84\x8d\xaa\x80\xc3*(Q" + + "\x82\xbb\xf7\xc0\xd4t\xf1g\x8f\x87h\xae\x1c\xd1\xb5\x85" + + "i\xf3\x94\xce\xa1\xf0\xd4d\xbf\x7f\x8b\xaa\x01B\xaa\xed" + + "Vr\xc5\x1fBO\xbd[\x89d\x83\xd3\x8a\x0c\xf6(" + + "pg\x19\x83SI\xef\x86\xb0\xf7\x85\x8a\x811#\x9d" + + "\xc3\x95\xf6\x1f\xb2\xf9\x9d\xcct~\xd1\xeb\x96\xde\x16B" + + "\xf0\x9a\xbf\xb4\xd5IfD\x87\xe2\xba?\xf3\x19\xfc=" + + "\xd4\xcf\xc5\xdf\xcf\xb5\xef\xf6\x9c\x817\x11\xe5~!\x8f" + + "\xf2\xa2@\xeeYk?o\xa2\xa4?\x0c\x11y\xae4" + + "\x0d\xa1\x8b\xa2\xc0\xa3\xbc\x8c\xc3(=^1\xe6\x0f\x87" + + "{\x19\xdd\xfb\xc1NPhQ3\x0cp\x89\x8b\xe6\xc0" + + "\xf5\xe1\x8dI\x07\xef\xce\x86\xe6\xb6\xdb\xf5\x0e\x1apo" + + "\xf4Xv\xf2E\xdfe\x0d\xce\xa1\x84\xb3\xd1\xf6\x1c\xc6" + + "\x1d\xc3\xa2;\xd0\x13\xf7,\x05N\xdc)\xa0?\xaaD" + + "w2)n\xd3\x81\x137\x0b\xc8y\x83mt\x07\xd8" + + "\xe2\xfa\x07\x80\x13\xd7\x0a\xc8{sitGb\xf5=" + + "\xc3\x108q\xb9\x80!o\xde\x8f\xee@M\\\xdc\x05" + + "\x9c\x98\x150\xec\x8d\xbc\xd1\x9d\xb9\x8a\xb7\xaf\x06N\x9c" + + "\xef\x0f~\xa0\xc1\xf1\xa3\x11-\x17\xa3Pk\xa3\xb4\xf7" + + "\x18\xc8\xd1\x02hD\xcb\xed\x81\xf9\x8b5\xc1\xb6\x96;" + + "\xc9\x80hZ1Y#5gN\xfdc\x89\x00\xa0\x11" + + "\xe5\x10\x06\xe6\x89\x00\x97\xfb\x08M\xb2Z;\xcf?\xb4" + + "er\xd7\xff@J\xe2\xfb\xb3\x9a\xce\xf1&b\x81}" + + "\xa9\x0b\xac\xe6Q\x1e\xcd\x0d\xda\xf8\x85.\xe6\x85\x0b\xfe" + + "(-\xa6\xfd\xff\xd6\xdb\xff05N\xef\xf2(\x1f\x0b" + + "\x94\xf5Q\x12~\xc0\xa3|<\xd08}D\xb5~\x8c" + + "G\xf9\xbc?\xe4\xfc\xea\x01\x00\xf9<\x8f\xc9@#\"" + + "\xfe\x89\x14\xbf\xa3\xeb\xdanC\xd0iC\xc2\xb8\x11 " + + "UI\xd7x\xdcnCBN\x1b\"b;@*F" + + "\xf2+\x82m\xc8\x18\xbc\x0d 5\x9a\xe4\xe3\xb1\xf7\xbb" + + "F(\xea~\xa3\x96\xd3:ge\xd5~\xef6w\xea" + + "\x8a\xe6L%\x9b+\xea\x0c\xfc\xab\xb5D6\xcd\x81\xdb" + + "\xde\x19\xc7:\x93\x97\x14\x810\x83\x867\x95\xb9\x84\x17" + + "\xe5\x90n\x9e\x19\xba\xae\xa1^\xd6\xc4N\xf6\x9bX\xaf" + + "\x87\xa5^\xfcf\x1e\xe5y\x94\x8aF'\x15r\xbb\xdf" + + "v\xd7\xa6\x95\xa2\xc1\xfa\xf8\x00<\xd3\xbd)\x80\xb1H" + + "+\xe62I\x06\x82\xa9\xf7\x94\x85`\xd0f6\xc5\xa2" + + ".s9\x13d\xf7\xbf!\xe8\xfe\xd3#0Av\xc7" + + "\xf8\xe8\xfeo\xab\xef\x04\xd9\x8dA\x9f\x09\xb2\xf3\xc1\xc6" + + "h\xef\x09\xf2e<_\x9dk,\xc0\x18\x974X\x1d" + + "\xf2<\xd2\xfb\xf7oY\xa5W]\xee\x98\xc0\xbd\x90\xfe" + + "\x1c\x00\x00\xff\xff\xa1\x1ap\xe9" func init() { schemas.Register(schema_db8274f9144abc7e,