From 80a15547e3331332a1fbb3a5ae0d01d2cdc7176b Mon Sep 17 00:00:00 2001 From: Chung-Ting Huang Date: Mon, 17 Jun 2019 16:18:47 -0500 Subject: [PATCH] TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge --- cmd/cloudflared/buildinfo/build_info.go | 28 ++ cmd/cloudflared/tunnel/cmd.go | 19 +- cmd/cloudflared/tunnel/configuration.go | 3 +- connection/connection.go | 135 ++----- connection/discovery.go | 147 +++++++- connection/discovery_test.go | 20 +- connection/manager.go | 281 ++++++++++++++ connection/manager_test.go | 77 ++++ connection/supervisor.go | 158 -------- origin/build_info.go | 19 - origin/supervisor.go | 22 +- origin/tunnel.go | 44 +-- tunnelrpc/pogs/config.go | 1 + tunnelrpc/tunnelrpc.capnp | 2 + tunnelrpc/tunnelrpc.capnp.go | 469 +++++++++++++----------- 15 files changed, 856 insertions(+), 569 deletions(-) create mode 100644 cmd/cloudflared/buildinfo/build_info.go create mode 100644 connection/manager.go create mode 100644 connection/manager_test.go delete mode 100644 connection/supervisor.go delete mode 100644 origin/build_info.go diff --git a/cmd/cloudflared/buildinfo/build_info.go b/cmd/cloudflared/buildinfo/build_info.go new file mode 100644 index 00000000..80481716 --- /dev/null +++ b/cmd/cloudflared/buildinfo/build_info.go @@ -0,0 +1,28 @@ +package buildinfo + +import ( + "runtime" + + "github.com/sirupsen/logrus" +) + +type BuildInfo struct { + GoOS string `json:"go_os"` + GoVersion string `json:"go_version"` + GoArch string `json:"go_arch"` + CloudflaredVersion string `json:"cloudflared_version"` +} + +func GetBuildInfo(cloudflaredVersion string) *BuildInfo { + return &BuildInfo{ + GoOS: runtime.GOOS, + GoVersion: runtime.Version(), + GoArch: runtime.GOARCH, + CloudflaredVersion: cloudflaredVersion, + } +} + +func (bi *BuildInfo) Log(logger *logrus.Logger) { + logger.Infof("Version %s", bi.CloudflaredVersion) + logger.Infof("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch) +} diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 14dcced0..447af863 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -1,6 +1,7 @@ package tunnel import ( + "context" "fmt" "io/ioutil" "net" @@ -12,8 +13,10 @@ import ( "time" "github.com/getsentry/raven-go" + "github.com/google/uuid" "golang.org/x/crypto/ssh/terminal" + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/cmd/cloudflared/updater" "github.com/cloudflare/cloudflared/cmd/sqlgateway" @@ -235,7 +238,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan return err } - buildInfo := origin.GetBuildInfo() + buildInfo := buildinfo.GetBuildInfo(version) logger.Infof("Build info: %+v", *buildInfo) logger.Infof("Version %s", version) logClientOptions(c) @@ -280,6 +283,18 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan go writePidFile(connectedSignal, c.String("pidfile")) } + cloudflaredID, err := uuid.NewRandom() + if err != nil { + logger.WithError(err).Error("cannot generate cloudflared ID") + return err + } + + ctx, cancel := context.WithCancel(context.Background()) + go func() { + <-shutdownC + cancel() + }() + // Serve DNS proxy stand-alone if no hostname or tag or app is going to run if dnsProxyStandAlone(c) { connectedSignal.Notify() @@ -324,7 +339,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan wg.Add(1) go func() { defer wg.Done() - errC <- origin.StartTunnelDaemon(tunnelConfig, graceShutdownC, connectedSignal) + errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, cloudflaredID) }() return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period")) diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index d0595d48..56f33f43 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/cmd/cloudflared/config" "github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/tlsconfig" @@ -145,7 +146,7 @@ If you don't have a certificate signed by Cloudflare, run the command: func prepareTunnelConfig( c *cli.Context, - buildInfo *origin.BuildInfo, + buildInfo *buildinfo.BuildInfo, version string, logger, transportLogger *logrus.Logger, ) (*origin.TunnelConfig, error) { diff --git a/connection/connection.go b/connection/connection.go index 0e830e91..984b7ba4 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -2,15 +2,14 @@ package connection import ( "context" - "crypto/tls" "net" - "sync" "time" "github.com/cloudflare/cloudflared/h2mux" - "github.com/cloudflare/cloudflared/streamhandler" "github.com/cloudflare/cloudflared/tunnelrpc" + "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" + "github.com/google/uuid" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -18,7 +17,6 @@ import ( ) const ( - dialTimeout = 5 * time.Second openStreamTimeout = 30 * time.Second ) @@ -30,123 +28,54 @@ func (e dialError) Error() string { return e.cause.Error() } -type muxerShutdownError struct{} - -func (e muxerShutdownError) Error() string { - return "muxer shutdown" +type Connection struct { + id uuid.UUID + muxer *h2mux.Muxer } -type ConnectionConfig struct { - TLSConfig *tls.Config - HeartbeatInterval time.Duration - MaxHeartbeats uint64 - Logger *logrus.Entry -} - -type connectionHandler interface { - serve(ctx context.Context) error - connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error) - shutdown() -} - -type h2muxHandler struct { - muxer *h2mux.Muxer - logger *logrus.Entry -} - -func (h *h2muxHandler) serve(ctx context.Context) error { - // Serve doesn't return until h2mux is shutdown - if err := h.muxer.Serve(ctx); err != nil { - return err +func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) { + id, err := uuid.NewRandom() + if err != nil { + return nil, err } - return muxerShutdownError{} + return &Connection{ + id: id, + muxer: muxer, + }, nil +} + +func (c *Connection) Serve(ctx context.Context) error { + // Serve doesn't return until h2mux is shutdown + return c.muxer.Serve(ctx) } // Connect is used to establish connections with cloudflare's edge network -func (h *h2muxHandler) connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters) (*tunnelpogs.ConnectResult, error) { +func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (*pogs.ConnectResult, error) { openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout) defer cancel() - conn, err := h.newRPConn(openStreamCtx) + + rpcConn, err := c.newRPConn(openStreamCtx, logger) if err != nil { - return nil, errors.Wrap(err, "Failed to create new RPC connection") + return nil, errors.Wrap(err, "cannot create new RPC connection") } - defer conn.Close() - tsClient := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)} + defer rpcConn.Close() + + tsClient := tunnelpogs.TunnelServer_PogsClient{Client: rpcConn.Bootstrap(ctx)} + return tsClient.Connect(ctx, parameters) } -func (h *h2muxHandler) shutdown() { - h.muxer.Shutdown() +func (c *Connection) Shutdown() { + c.muxer.Shutdown() } -func (h *h2muxHandler) newRPConn(ctx context.Context) (*rpc.Conn, error) { - stream, err := h.muxer.OpenRPCStream(ctx) +func (c *Connection) newRPConn(ctx context.Context, logger *logrus.Entry) (*rpc.Conn, error) { + stream, err := c.muxer.OpenRPCStream(ctx) if err != nil { return nil, err } return rpc.NewConn( - tunnelrpc.NewTransportLogger(h.logger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)), - tunnelrpc.ConnLog(h.logger.WithField("subsystem", "rpc-transport")), + tunnelrpc.NewTransportLogger(logger.WithField("rpc", "connect"), rpc.StreamTransport(stream)), + tunnelrpc.ConnLog(logger.WithField("rpc", "connect")), ), nil } - -// NewConnectionHandler returns a connectionHandler, wrapping h2mux to make RPC calls -func newH2MuxHandler(ctx context.Context, - streamHandler *streamhandler.StreamHandler, - config *ConnectionConfig, - edgeIP *net.TCPAddr, -) (connectionHandler, error) { - // Inherit from parent context so we can cancel (Ctrl-C) while dialing - dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout) - defer dialCancel() - dialer := net.Dialer{DualStack: true} - plaintextEdgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String()) - if err != nil { - return nil, dialError{cause: errors.Wrap(err, "DialContext error")} - } - edgeConn := tls.Client(plaintextEdgeConn, config.TLSConfig) - edgeConn.SetDeadline(time.Now().Add(dialTimeout)) - err = edgeConn.Handshake() - if err != nil { - return nil, dialError{cause: errors.Wrap(err, "Handshake with edge error")} - } - // clear the deadline on the conn; h2mux has its own timeouts - edgeConn.SetDeadline(time.Time{}) - // Establish a muxed connection with the edge - // Client mux handshake with agent server - muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{ - Timeout: dialTimeout, - Handler: streamHandler, - IsClient: true, - HeartbeatInterval: config.HeartbeatInterval, - MaxHeartbeats: config.MaxHeartbeats, - Logger: config.Logger, - }) - if err != nil { - return nil, err - } - return &h2muxHandler{ - muxer: muxer, - logger: config.Logger, - }, nil -} - -// connectionPool is a pool of connection handlers -type connectionPool struct { - sync.Mutex - connectionHandlers []connectionHandler -} - -func (cp *connectionPool) put(h connectionHandler) { - cp.Lock() - defer cp.Unlock() - cp.connectionHandlers = append(cp.connectionHandlers, h) -} - -func (cp *connectionPool) close() { - cp.Lock() - defer cp.Unlock() - for _, h := range cp.connectionHandlers { - h.shutdown() - } -} diff --git a/connection/discovery.go b/connection/discovery.go index 898b0755..7170f760 100644 --- a/connection/discovery.go +++ b/connection/discovery.go @@ -5,10 +5,11 @@ import ( "crypto/tls" "fmt" "net" + "sync" "time" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" + "github.com/sirupsen/logrus" ) const ( @@ -22,6 +23,9 @@ const ( dotServerName = "cloudflare-dns.com" dotServerAddr = "1.1.1.1:853" dotTimeout = time.Duration(15 * time.Second) + + // SRV record resolution TTL + resolveEdgeAddrTTL = 1 * time.Hour ) var friendlyDNSErrorLines = []string{ @@ -34,20 +38,65 @@ var friendlyDNSErrorLines = []string{ ` https://developers.cloudflare.com/1.1.1.1/setting-up-1.1.1.1/`, } -func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, error) { - if len(addresses) > 0 { - var tcpAddrs []*net.TCPAddr - for _, address := range addresses { - // Addresses specified (for testing, usually) - tcpAddr, err := net.ResolveTCPAddr("tcp", address) - if err != nil { - return nil, err - } - tcpAddrs = append(tcpAddrs, tcpAddr) - } - return tcpAddrs, nil +// EdgeServiceDiscoverer is an interface for looking up Cloudflare's edge network addresses +type EdgeServiceDiscoverer interface { + // Addr returns an address to connect to cloudflare's edge network + Addr() *net.TCPAddr + // AvailableAddrs returns the number of unique addresses + AvailableAddrs() uint8 + // Refresh rediscover Cloudflare's edge network addresses + Refresh() error +} + +// EdgeAddrResolver discovers the addresses of Cloudflare's edge network through SRV record. +// It implements EdgeServiceDiscoverer interface +type EdgeAddrResolver struct { + sync.Mutex + // Addrs to connect to cloudflare's edge network + addrs []*net.TCPAddr + // index of the next element to use in addrs + nextAddrIndex int + logger *logrus.Entry +} + +func NewEdgeAddrResolver(logger *logrus.Logger) (EdgeServiceDiscoverer, error) { + r := &EdgeAddrResolver{ + logger: logger.WithField("subsystem", " edgeAddrResolver"), } - // HA service discovery lookup + if err := r.Refresh(); err != nil { + return nil, err + } + return r, nil +} + +func (r *EdgeAddrResolver) Addr() *net.TCPAddr { + r.Lock() + defer r.Unlock() + addr := r.addrs[r.nextAddrIndex] + r.nextAddrIndex = (r.nextAddrIndex + 1) % len(r.addrs) + return addr +} + +func (r *EdgeAddrResolver) AvailableAddrs() uint8 { + r.Lock() + defer r.Unlock() + return uint8(len(r.addrs)) +} + +func (r *EdgeAddrResolver) Refresh() error { + newAddrs, err := EdgeDiscovery(r.logger) + if err != nil { + return err + } + r.Lock() + defer r.Unlock() + r.addrs = newAddrs + r.nextAddrIndex = 0 + return nil +} + +// HA service discovery lookup +func EdgeDiscovery(logger *logrus.Entry) ([]*net.TCPAddr, error) { _, addrs, err := net.LookupSRV(srvService, srvProto, srvName) if err != nil { // Try to fall back to DoT from Cloudflare directly. @@ -78,7 +127,7 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err var resolvedIPsPerCNAME [][]*net.TCPAddr var lookupErr error for _, addr := range addrs { - ips, err := ResolveSRVToTCP(addr) + ips, err := resolveSRVToTCP(addr) if err != nil || len(ips) == 0 { // don't return early, we might be able to resolve other addresses lookupErr = err @@ -86,14 +135,14 @@ func ResolveEdgeIPs(logger *log.Logger, addresses []string) ([]*net.TCPAddr, err } resolvedIPsPerCNAME = append(resolvedIPsPerCNAME, ips) } - ips := FlattenServiceIPs(resolvedIPsPerCNAME) + ips := flattenServiceIPs(resolvedIPsPerCNAME) if lookupErr == nil && len(ips) == 0 { return nil, fmt.Errorf("Unknown service discovery error") } return ips, lookupErr } -func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) { +func resolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) { ips, err := net.LookupIP(srv.Target) if err != nil { return nil, err @@ -107,7 +156,7 @@ func ResolveSRVToTCP(srv *net.SRV) ([]*net.TCPAddr, error) { // FlattenServiceIPs transposes and flattens the input slices such that the // first element of the n inner slices are the first n elements of the result. -func FlattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr { +func flattenServiceIPs(ipsByService [][]*net.TCPAddr) []*net.TCPAddr { var result []*net.TCPAddr for len(ipsByService) > 0 { filtered := ipsByService[:0] @@ -141,3 +190,65 @@ func fallbackResolver(serverName, serverAddress string) *net.Resolver { }, } } + +// EdgeHostnameResolver discovers the addresses of Cloudflare's edge network via a list of server hostnames. +// It implements EdgeServiceDiscoverer interface, and is used mainly for testing connectivity. +type EdgeHostnameResolver struct { + sync.Mutex + // hostnames of edge servers + hostnames []string + // Addrs to connect to cloudflare's edge network + addrs []*net.TCPAddr + // index of the next element to use in addrs + nextAddrIndex int +} + +func NewEdgeHostnameResolver(edgeHostnames []string) (EdgeServiceDiscoverer, error) { + r := &EdgeHostnameResolver{ + hostnames: edgeHostnames, + } + if err := r.Refresh(); err != nil { + return nil, err + } + return r, nil +} + +func (r *EdgeHostnameResolver) Addr() *net.TCPAddr { + r.Lock() + defer r.Unlock() + addr := r.addrs[r.nextAddrIndex] + r.nextAddrIndex = (r.nextAddrIndex + 1) % len(r.addrs) + return addr +} + +func (r *EdgeHostnameResolver) AvailableAddrs() uint8 { + r.Lock() + defer r.Unlock() + return uint8(len(r.addrs)) +} + +func (r *EdgeHostnameResolver) Refresh() error { + newAddrs, err := ResolveAddrs(r.hostnames) + if err != nil { + return err + } + r.Lock() + defer r.Unlock() + r.addrs = newAddrs + r.nextAddrIndex = 0 + return nil +} + +// Resolve TCP address given a list of addresses. Address can be a hostname, however, it will return at most one +// of the hostname's IP addresses +func ResolveAddrs(addrs []string) ([]*net.TCPAddr, error) { + var tcpAddrs []*net.TCPAddr + for _, addr := range addrs { + tcpAddr, err := net.ResolveTCPAddr("tcp", addr) + if err != nil { + return nil, err + } + tcpAddrs = append(tcpAddrs, tcpAddr) + } + return tcpAddrs, nil +} diff --git a/connection/discovery_test.go b/connection/discovery_test.go index 4e5aeacf..806df8bb 100644 --- a/connection/discovery_test.go +++ b/connection/discovery_test.go @@ -7,8 +7,26 @@ import ( "github.com/stretchr/testify/assert" ) +type mockEdgeServiceDiscoverer struct { +} + +func (mr *mockEdgeServiceDiscoverer) Addr() *net.TCPAddr { + return &net.TCPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 63102, + } +} + +func (mr *mockEdgeServiceDiscoverer) AvailableAddrs() uint8 { + return 1 +} + +func (mr *mockEdgeServiceDiscoverer) Refresh() error { + return nil +} + func TestFlattenServiceIPs(t *testing.T) { - result := FlattenServiceIPs([][]*net.TCPAddr{ + result := flattenServiceIPs([][]*net.TCPAddr{ []*net.TCPAddr{ &net.TCPAddr{Port: 1}, &net.TCPAddr{Port: 2}, diff --git a/connection/manager.go b/connection/manager.go new file mode 100644 index 00000000..e266ba0f --- /dev/null +++ b/connection/manager.go @@ -0,0 +1,281 @@ +package connection + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "sync" + "time" + + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" + "github.com/cloudflare/cloudflared/h2mux" + "github.com/cloudflare/cloudflared/tunnelrpc/pogs" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const ( + quickStartLink = "https://developers.cloudflare.com/argo-tunnel/quickstart/" + faqLink = "https://developers.cloudflare.com/argo-tunnel/faq/" +) + +// EdgeManager manages connections with the edge +type EdgeManager struct { + // streamHandler handles stream opened by the edge + streamHandler h2mux.MuxedStreamHandler + // TLSConfig is the TLS configuration to connect with edge + tlsConfig *tls.Config + // cloudflaredConfig is the cloudflared configuration that is determined when the process first starts + cloudflaredConfig *CloudflaredConfig + // serviceDiscoverer returns the next edge addr to connect to + serviceDiscoverer EdgeServiceDiscoverer + // state is attributes of ConnectionManager that can change during runtime. + state *edgeManagerState + + logger *logrus.Entry +} + +// EdgeConnectionManagerConfigurable is the configurable attributes of a EdgeConnectionManager +type EdgeManagerConfigurable struct { + TunnelHostnames []h2mux.TunnelHostname + *pogs.EdgeConnectionConfig +} + +type CloudflaredConfig struct { + CloudflaredID uuid.UUID + Tags []pogs.Tag + BuildInfo *buildinfo.BuildInfo +} + +func NewEdgeManager( + streamHandler h2mux.MuxedStreamHandler, + edgeConnMgrConfigurable *EdgeManagerConfigurable, + userCredential []byte, + tlsConfig *tls.Config, + serviceDiscoverer EdgeServiceDiscoverer, + cloudflaredConfig *CloudflaredConfig, + logger *logrus.Logger, +) *EdgeManager { + return &EdgeManager{ + streamHandler: streamHandler, + tlsConfig: tlsConfig, + cloudflaredConfig: cloudflaredConfig, + serviceDiscoverer: serviceDiscoverer, + state: newEdgeConnectionManagerState(edgeConnMgrConfigurable, userCredential), + logger: logger.WithField("subsystem", "connectionManager"), + } +} + +func (em *EdgeManager) Run(ctx context.Context) error { + defer em.shutdown() + + resolveEdgeIPTicker := time.Tick(resolveEdgeAddrTTL) + for { + select { + case <-ctx.Done(): + return errors.Wrap(ctx.Err(), "EdgeConnectionManager terminated") + case <-resolveEdgeIPTicker: + if err := em.serviceDiscoverer.Refresh(); err != nil { + em.logger.WithError(err).Warn("Cannot refresh Cloudflare edge addresses") + } + default: + time.Sleep(1 * time.Second) + } + // Create/delete connection one at a time, so we don't need to adjust for connections that are being created/deleted + // in shouldCreateConnection or shouldReduceConnection calculation + if em.state.shouldCreateConnection(em.serviceDiscoverer.AvailableAddrs()) { + if err := em.newConnection(ctx); err != nil { + em.logger.WithError(err).Error("cannot create new connection") + } + } else if em.state.shouldReduceConnection() { + if err := em.closeConnection(ctx); err != nil { + em.logger.WithError(err).Error("cannot close connection") + } + } + } +} + +func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurable) { + em.logger.Infof("New edge connection manager configuration %+v", newConfigurable) + em.state.updateConfigurable(newConfigurable) +} + +func (em *EdgeManager) newConnection(ctx context.Context) error { + edgeIP := em.serviceDiscoverer.Addr() + edgeConn, err := em.dialEdge(ctx, edgeIP) + if err != nil { + return errors.Wrap(err, "dial edge error") + } + configurable := em.state.getConfigurable() + // Establish a muxed connection with the edge + // Client mux handshake with agent server + muxer, err := h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{ + Timeout: configurable.Timeout, + Handler: em.streamHandler, + IsClient: true, + HeartbeatInterval: configurable.HeartbeatInterval, + MaxHeartbeats: configurable.MaxFailedHeartbeats, + Logger: em.logger.WithField("subsystem", "muxer"), + }) + if err != nil { + return errors.Wrap(err, "handshake with edge error") + } + + h2muxConn, err := newConnection(muxer, edgeIP) + if err != nil { + return errors.Wrap(err, "create h2mux connection error") + } + + go em.serveConn(ctx, h2muxConn) + + connResult, err := h2muxConn.Connect(ctx, &pogs.ConnectParameters{ + OriginCert: em.state.getUserCredential(), + CloudflaredID: em.cloudflaredConfig.CloudflaredID, + NumPreviousAttempts: 0, + CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion, + }, em.logger) + if err != nil { + h2muxConn.Shutdown() + return errors.Wrap(err, "connect with edge error") + } + + if connErr := connResult.Err; connErr != nil { + if !connErr.ShouldRetry { + return errors.Wrap(connErr, em.noRetryMessage()) + } + return errors.Wrapf(connErr, "server respond with retry at %v", connErr.RetryAfter) + } + + em.state.newConnection(h2muxConn) + em.logger.Infof("connected to %s", connResult.ServerInfo.LocationName) + return nil +} + +func (em *EdgeManager) closeConnection(ctx context.Context) error { + conn := em.state.getFirstConnection() + if conn == nil { + return fmt.Errorf("no connection to close") + } + conn.Shutdown() + return nil +} + +func (em *EdgeManager) serveConn(ctx context.Context, conn *Connection) { + err := conn.Serve(ctx) + em.logger.WithError(err).Warn("Connection closed") + em.state.closeConnection(conn) +} + +func (em *EdgeManager) dialEdge(ctx context.Context, edgeIP *net.TCPAddr) (*tls.Conn, error) { + timeout := em.state.getConfigurable().Timeout + // Inherit from parent context so we can cancel (Ctrl-C) while dialing + dialCtx, dialCancel := context.WithTimeout(ctx, timeout) + defer dialCancel() + + dialer := net.Dialer{DualStack: true} + edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String()) + if err != nil { + return nil, dialError{cause: errors.Wrap(err, "DialContext error")} + } + tlsEdgeConn := tls.Client(edgeConn, em.tlsConfig) + tlsEdgeConn.SetDeadline(time.Now().Add(timeout)) + + if err = tlsEdgeConn.Handshake(); err != nil { + return nil, dialError{cause: errors.Wrap(err, "Handshake with edge error")} + } + // clear the deadline on the conn; h2mux has its own timeouts + tlsEdgeConn.SetDeadline(time.Time{}) + return tlsEdgeConn, nil +} + +func (em *EdgeManager) noRetryMessage() string { + messageTemplate := "cloudflared could not register an Argo Tunnel on your account. Please confirm the following before trying again:" + + "1. You have Argo Smart Routing enabled in your account, See Enable Argo section of %s." + + "2. Your credential at %s is still valid. See %s." + return fmt.Sprintf(messageTemplate, quickStartLink, em.state.getConfigurable().UserCredentialPath, faqLink) +} + +func (em *EdgeManager) shutdown() { + em.state.shutdown() +} + +type edgeManagerState struct { + sync.RWMutex + configurable *EdgeManagerConfigurable + userCredential []byte + conns map[uuid.UUID]*Connection +} + +func newEdgeConnectionManagerState(configurable *EdgeManagerConfigurable, userCredential []byte) *edgeManagerState { + return &edgeManagerState{ + configurable: configurable, + userCredential: userCredential, + conns: make(map[uuid.UUID]*Connection), + } +} + +func (ems *edgeManagerState) shouldCreateConnection(availableEdgeAddrs uint8) bool { + ems.RLock() + defer ems.RUnlock() + expectedHAConns := ems.configurable.NumHAConnections + if availableEdgeAddrs < expectedHAConns { + expectedHAConns = availableEdgeAddrs + } + return uint8(len(ems.conns)) < expectedHAConns +} + +func (ems *edgeManagerState) shouldReduceConnection() bool { + ems.RLock() + defer ems.RUnlock() + return uint8(len(ems.conns)) > ems.configurable.NumHAConnections +} + +func (ems *edgeManagerState) newConnection(conn *Connection) { + ems.Lock() + defer ems.Unlock() + ems.conns[conn.id] = conn +} + +func (ems *edgeManagerState) closeConnection(conn *Connection) { + ems.Lock() + defer ems.Unlock() + delete(ems.conns, conn.id) +} + +func (ems *edgeManagerState) getFirstConnection() *Connection { + ems.RLock() + defer ems.RUnlock() + + for _, conn := range ems.conns { + return conn + } + return nil +} + +func (ems *edgeManagerState) shutdown() { + ems.Lock() + defer ems.Unlock() + for _, conn := range ems.conns { + conn.Shutdown() + } +} + +func (ems *edgeManagerState) getConfigurable() *EdgeManagerConfigurable { + ems.Lock() + defer ems.Unlock() + return ems.configurable +} + +func (ems *edgeManagerState) updateConfigurable(newConfigurable *EdgeManagerConfigurable) { + ems.Lock() + defer ems.Unlock() + ems.configurable = newConfigurable +} + +func (ems *edgeManagerState) getUserCredential() []byte { + ems.RLock() + defer ems.RUnlock() + return ems.userCredential +} diff --git a/connection/manager_test.go b/connection/manager_test.go new file mode 100644 index 00000000..7565567f --- /dev/null +++ b/connection/manager_test.go @@ -0,0 +1,77 @@ +package connection + +import ( + "testing" + "time" + + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" + "github.com/stretchr/testify/assert" + + "github.com/cloudflare/cloudflared/h2mux" + "github.com/cloudflare/cloudflared/tunnelrpc/pogs" + "github.com/google/uuid" + "github.com/sirupsen/logrus" +) + +var ( + configurable = &EdgeManagerConfigurable{ + []h2mux.TunnelHostname{ + "http.example.com", + "ws.example.com", + "hello.example.com", + }, + &pogs.EdgeConnectionConfig{ + NumHAConnections: 1, + HeartbeatInterval: 1 * time.Second, + Timeout: 5 * time.Second, + MaxFailedHeartbeats: 3, + UserCredentialPath: "/etc/cloudflared/cert.pem", + }, + } + cloudflaredConfig = &CloudflaredConfig{ + CloudflaredID: uuid.New(), + Tags: []pogs.Tag{ + {Name: "pool", Value: "east-6"}, + }, + BuildInfo: &buildinfo.BuildInfo{ + GoOS: "linux", + GoVersion: "1.12", + GoArch: "amd64", + CloudflaredVersion: "2019.6.0", + }, + } +) + +type mockStreamHandler struct { +} + +func (msh *mockStreamHandler) ServeStream(*h2mux.MuxedStream) error { + return nil +} + +func mockEdgeManager() *EdgeManager { + return NewEdgeManager( + &mockStreamHandler{}, + configurable, + []byte{}, + nil, + &mockEdgeServiceDiscoverer{}, + cloudflaredConfig, + logrus.New(), + ) +} + +func TestUpdateConfigurable(t *testing.T) { + m := mockEdgeManager() + newConfigurable := &EdgeManagerConfigurable{ + []h2mux.TunnelHostname{ + "second.example.com", + }, + &pogs.EdgeConnectionConfig{ + NumHAConnections: 2, + }, + } + m.UpdateConfigurable(newConfigurable) + + assert.Equal(t, newConfigurable, m.state.getConfigurable()) +} diff --git a/connection/supervisor.go b/connection/supervisor.go deleted file mode 100644 index ba39043a..00000000 --- a/connection/supervisor.go +++ /dev/null @@ -1,158 +0,0 @@ -package connection - -import ( - "context" - "net" - "time" - - "github.com/cloudflare/cloudflared/streamhandler" - - "github.com/cloudflare/cloudflared/tunnelrpc/pogs" - tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" -) - -const ( - // Waiting time before retrying a failed tunnel connection - reconnectDuration = time.Second * 10 - // SRV record resolution TTL - resolveTTL = time.Hour - // Interval between establishing new connection - connectionInterval = time.Second -) - -type CloudflaredConfig struct { - ConnectionConfig *ConnectionConfig - OriginCert []byte - Tags []tunnelpogs.Tag - EdgeAddrs []string - HAConnections uint - Logger *logrus.Logger - CloudflaredVersion string -} - -// Supervisor is a stateful object that manages connections with the edge -type Supervisor struct { - streamHandler *streamhandler.StreamHandler - newConfigChan chan<- *pogs.ClientConfig - useConfigResultChan <-chan *pogs.UseConfigurationResult - config *CloudflaredConfig - state *supervisorState - connErrors chan error -} - -type supervisorState struct { - // IPs to connect to cloudflare's edge network - edgeIPs []*net.TCPAddr - // index of the next element to use in edgeIPs - nextEdgeIPIndex int - // last time edgeIPs were refreshed - lastResolveTime time.Time - // ID of this cloudflared instance - cloudflaredID uuid.UUID - // connectionPool is a pool of connectionHandlers that can be used to make RPCs - connectionPool *connectionPool -} - -func (s *supervisorState) getNextEdgeIP() *net.TCPAddr { - ip := s.edgeIPs[s.nextEdgeIPIndex%len(s.edgeIPs)] - s.nextEdgeIPIndex++ - return ip -} - -func NewSupervisor(config *CloudflaredConfig) *Supervisor { - newConfigChan := make(chan *pogs.ClientConfig) - useConfigResultChan := make(chan *pogs.UseConfigurationResult) - return &Supervisor{ - streamHandler: streamhandler.NewStreamHandler(newConfigChan, useConfigResultChan, config.Logger), - newConfigChan: newConfigChan, - useConfigResultChan: useConfigResultChan, - config: config, - state: &supervisorState{ - connectionPool: &connectionPool{}, - }, - connErrors: make(chan error), - } -} - -func (s *Supervisor) Run(ctx context.Context) error { - logger := s.config.Logger - if err := s.initialize(); err != nil { - logger.WithError(err).Error("Failed to get edge IPs") - return err - } - defer s.state.connectionPool.close() - - var currentConnectionCount uint - expectedConnectionCount := s.config.HAConnections - if uint(len(s.state.edgeIPs)) < s.config.HAConnections { - logger.Warnf("You requested %d HA connections but I can give you at most %d.", s.config.HAConnections, len(s.state.edgeIPs)) - expectedConnectionCount = uint(len(s.state.edgeIPs)) - } - for { - select { - case <-ctx.Done(): - return nil - case connErr := <-s.connErrors: - logger.WithError(connErr).Warnf("Connection dropped unexpectedly") - currentConnectionCount-- - default: - time.Sleep(5 * time.Second) - } - if currentConnectionCount < expectedConnectionCount { - h, err := newH2MuxHandler(ctx, s.streamHandler, s.config.ConnectionConfig, s.state.getNextEdgeIP()) - if err != nil { - logger.WithError(err).Error("Failed to create new connection handler") - continue - } - go func() { - s.connErrors <- h.serve(ctx) - }() - connResult, err := s.connect(ctx, s.config, s.state.cloudflaredID, h) - if err != nil { - logger.WithError(err).Errorf("Failed to connect to cloudflared's edge network") - h.shutdown() - continue - } - if connErr := connResult.Err; connErr != nil && !connErr.ShouldRetry { - logger.WithError(connErr).Errorf("Server respond with don't retry to connect") - h.shutdown() - return err - } - logger.Infof("Connected to %s", connResult.ServerInfo.LocationName) - s.state.connectionPool.put(h) - currentConnectionCount++ - } - } -} - -func (s *Supervisor) initialize() error { - edgeIPs, err := ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs) - if err != nil { - return errors.Wrapf(err, "Failed to resolve cloudflare edge network address") - } - s.state.edgeIPs = edgeIPs - s.state.lastResolveTime = time.Now() - cloudflaredID, err := uuid.NewRandom() - if err != nil { - return errors.Wrap(err, "Failed to generate cloudflared ID") - } - s.state.cloudflaredID = cloudflaredID - return nil -} - -func (s *Supervisor) connect(ctx context.Context, - config *CloudflaredConfig, - cloudflaredID uuid.UUID, - h connectionHandler, -) (*tunnelpogs.ConnectResult, error) { - connectParameters := &tunnelpogs.ConnectParameters{ - OriginCert: config.OriginCert, - CloudflaredID: cloudflaredID, - NumPreviousAttempts: 0, - CloudflaredVersion: config.CloudflaredVersion, - } - return h.connect(ctx, connectParameters) -} diff --git a/origin/build_info.go b/origin/build_info.go deleted file mode 100644 index 72f0965a..00000000 --- a/origin/build_info.go +++ /dev/null @@ -1,19 +0,0 @@ -package origin - -import ( - "runtime" -) - -type BuildInfo struct { - GoOS string `json:"go_os"` - GoVersion string `json:"go_version"` - GoArch string `json:"go_arch"` -} - -func GetBuildInfo() *BuildInfo { - return &BuildInfo{ - GoOS: runtime.GOOS, - GoVersion: runtime.Version(), - GoArch: runtime.GOARCH, - } -} diff --git a/origin/supervisor.go b/origin/supervisor.go index 9f0c352d..ff7d96f9 100644 --- a/origin/supervisor.go +++ b/origin/supervisor.go @@ -6,6 +6,8 @@ import ( "net" "time" + "github.com/sirupsen/logrus" + "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/signal" @@ -34,6 +36,8 @@ type Supervisor struct { // currently-connecting tunnels to finish connecting so we can reset backoff timer nextConnectedIndex int nextConnectedSignal chan struct{} + + logger *logrus.Entry } type resolveResult struct { @@ -51,6 +55,7 @@ func NewSupervisor(config *TunnelConfig) *Supervisor { config: config, tunnelErrors: make(chan tunnelError), tunnelsConnecting: map[int]chan struct{}{}, + logger: config.Logger.WithField("subsystem", "supervisor"), } } @@ -124,8 +129,10 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u } func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error { - logger := s.config.Logger - edgeIPs, err := connection.ResolveEdgeIPs(logger, s.config.EdgeAddrs) + logger := s.logger + + edgeIPs, err := s.resolveEdgeIPs() + if err != nil { logger.Infof("ResolveEdgeIPs err") return err @@ -215,6 +222,15 @@ func (s *Supervisor) getEdgeIP(index int) *net.TCPAddr { return s.edgeIPs[index%len(s.edgeIPs)] } +func (s *Supervisor) resolveEdgeIPs() ([]*net.TCPAddr, error) { + // If --edge is specfied, resolve edge server addresses + if len(s.config.EdgeAddrs) > 0 { + return connection.ResolveAddrs(s.config.EdgeAddrs) + } + // Otherwise lookup edge server addresses through service discovery + return connection.EdgeDiscovery(s.logger) +} + func (s *Supervisor) refreshEdgeIPs() { if s.resolverC != nil { return @@ -224,7 +240,7 @@ func (s *Supervisor) refreshEdgeIPs() { } s.resolverC = make(chan resolveResult) go func() { - edgeIPs, err := connection.ResolveEdgeIPs(s.config.Logger, s.config.EdgeAddrs) + edgeIPs, err := s.resolveEdgeIPs() s.resolverC <- resolveResult{edgeIPs: edgeIPs, err: err} }() } diff --git a/origin/tunnel.go b/origin/tunnel.go index 64539048..94b13deb 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -14,7 +14,7 @@ import ( "sync" "time" - "github.com/cloudflare/cloudflared/connection" + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/streamhandler" @@ -42,7 +42,7 @@ const ( ) type TunnelConfig struct { - BuildInfo *BuildInfo + BuildInfo *buildinfo.BuildInfo ClientID string ClientTlsConfig *tls.Config CloseConnOnce *sync.Once // Used to close connectedSignal no more than once @@ -140,44 +140,8 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str } } -func StartTunnelDaemon(config *TunnelConfig, shutdownC <-chan struct{}, connectedSignal *signal.Signal) error { - ctx, cancel := context.WithCancel(context.Background()) - go func() { - <-shutdownC - cancel() - }() - - u, err := uuid.NewRandom() - if err != nil { - return err - } - - // If a user specified negative HAConnections, we will treat it as requesting 1 connection - if config.HAConnections > 1 { - if config.UseDeclarativeTunnel { - return connection.NewSupervisor(&connection.CloudflaredConfig{ - ConnectionConfig: &connection.ConnectionConfig{ - TLSConfig: config.TlsConfig, - HeartbeatInterval: config.HeartbeatInterval, - MaxHeartbeats: config.MaxHeartbeats, - Logger: config.Logger.WithField("subsystem", "connection_supervisor"), - }, - OriginCert: config.OriginCert, - Tags: config.Tags, - EdgeAddrs: config.EdgeAddrs, - HAConnections: uint(config.HAConnections), - Logger: config.Logger, - CloudflaredVersion: config.ReportedVersion, - }).Run(ctx) - } - return NewSupervisor(config).Run(ctx, connectedSignal, u) - } else { - addrs, err := connection.ResolveEdgeIPs(config.Logger, config.EdgeAddrs) - if err != nil { - return err - } - return ServeTunnelLoop(ctx, config, addrs[0], 0, connectedSignal, u) - } +func StartTunnelDaemon(ctx context.Context, config *TunnelConfig, connectedSignal *signal.Signal, cloudflaredID uuid.UUID) error { + return NewSupervisor(config).Run(ctx, connectedSignal, cloudflaredID) } func ServeTunnelLoop(ctx context.Context, diff --git a/tunnelrpc/pogs/config.go b/tunnelrpc/pogs/config.go index bf8ebf23..f63c30e9 100644 --- a/tunnelrpc/pogs/config.go +++ b/tunnelrpc/pogs/config.go @@ -72,6 +72,7 @@ type EdgeConnectionConfig struct { HeartbeatInterval time.Duration Timeout time.Duration MaxFailedHeartbeats uint64 + UserCredentialPath string } // FailReason impelents FallibleConfig interface for EdgeConnectionConfig diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index 874f4d4e..1ce9218b 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -117,6 +117,8 @@ struct EdgeConnectionConfig { # closing the connection to the edge. # cloudflared CLI option: `heartbeat-count` maxFailedHeartbeats @3 :UInt64; + # Absolute path of the file containing certificate and token to connect with the edge + userCredentialPath @4 :Text; } struct ReverseProxyConfig { diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index cf6ea7aa..fdd276ed 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -1078,12 +1078,12 @@ type EdgeConnectionConfig struct{ capnp.Struct } const EdgeConnectionConfig_TypeID = 0xc744e349009087aa func NewEdgeConnectionConfig(s *capnp.Segment) (EdgeConnectionConfig, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 0}) + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 1}) return EdgeConnectionConfig{st}, err } func NewRootEdgeConnectionConfig(s *capnp.Segment) (EdgeConnectionConfig, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 0}) + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 32, PointerCount: 1}) return EdgeConnectionConfig{st}, err } @@ -1129,12 +1129,31 @@ func (s EdgeConnectionConfig) SetMaxFailedHeartbeats(v uint64) { s.Struct.SetUint64(24, v) } +func (s EdgeConnectionConfig) UserCredentialPath() (string, error) { + p, err := s.Struct.Ptr(0) + return p.Text(), err +} + +func (s EdgeConnectionConfig) HasUserCredentialPath() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s EdgeConnectionConfig) UserCredentialPathBytes() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return p.TextBytes(), err +} + +func (s EdgeConnectionConfig) SetUserCredentialPath(v string) error { + return s.Struct.SetText(0, v) +} + // EdgeConnectionConfig_List is a list of EdgeConnectionConfig. type EdgeConnectionConfig_List struct{ capnp.List } // NewEdgeConnectionConfig creates a new list of EdgeConnectionConfig. func NewEdgeConnectionConfig_List(s *capnp.Segment, sz int32) (EdgeConnectionConfig_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 32, PointerCount: 0}, sz) + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 32, PointerCount: 1}, sz) return EdgeConnectionConfig_List{l}, err } @@ -3723,227 +3742,229 @@ func (p ClientService_useConfiguration_Results_Promise) Result() UseConfiguratio return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } -const schema_db8274f9144abc7e = "x\xda\xacY{p\\\xe5u?\xe7\xde\x95\xaedK" + - "\xde\xbd\xbe\x02#\x81f[\x97L\x82\xc1\x14\xe2\xd0\x82" + - "\xdaf\xf5\xb0\x1c\xad\xe3\xc7^=\x0c1f\xc6\xd7\xbb" + - "\x9f\xb4\xd7\xbe{\xef\xfa>l\xc95\xb1q\xa1\x80\xca" + - "\xc3&x\x06;\x90\xdan)\x81\xe2\x82\x09L\xc7\x14" + - "gB\xfa 4\x93!LC\xa7\xb4\xe9?\x01\xa63" + - "\xb4\x0c\x85$\xc3\xd0\xc1\xdc\xce\xf9\xeesW\x8bl:" + - "\xf5\x1f\xd6\xce\xd9\xefq\xbe\xdf9\xe7w\x1e{\x9d\xdf" + - "1(\\\xdf\xf6J7\x80z\xa2\xad\xdd\xff\xfd\xdak" + - "\xa7~\xe7\xe8\x8f\xef\x04\xb9O\xf0\xbf\xf9\xd2\xfa\x9e\x8f" + - "\xddC\xff\x06\x80k^m\xdf\x87\xca\xbf\xb7K\x00\xca" + - "\x9b\xed\x9b\x01\xfd\x7f\xban\xff\xdb\xdb\x7fy\xe4\x1e\x90" + - "\xfb0Y\x99\x91\x00\xd6|\xd0>\x8fJ\xa7$\x81\xe8" + - "?vk\xcf?\xe2\x89\x8f\x8e\x80\xfc%\x04hC\xfa" + - "\xfa\x9d\xf6%\x02\xa0r\xbe\xbd\x00\xe8\xbfv\xcdK/" + - "\x1e\xfe\xde\xdd\xdf\x06\xf5\x8b\x88\x10\xec\xef\x97\xfe\x07\x01" + - "\x95\xeb%Z\xf0\xc1\x9f_\x9d9\xfd\xda\xf2\xef\xf0\x05" + - "\xfe\xe3\xaf\xdf\xfc\xdc\xe1\xef\xfd\xc6\xbb0%H\x98\x01" + - "X\xf3\x0d\xc9\xa6\xb5L\xfa\x0f@\xff\x81\x1f\xae\xb0\x86" + - "\xfes\xfb\xc9F\x9d\x82[G;\x06P\x99\xea\xa0\x07" + - "\xa8\x1dt\xf0\xc3\xffrnS\xed\xc8\xf1S \x7f1" + - "\xbaxw\x87 @\xc6\xbf\xe1_\xdf\xd9\xbc\xf1\xb9\xe9" + - "'\x82o\x82\xed\xac\xe39\xba\xc7\xe3[\x7f\xb47w" + - "\xdf\xd0\xef>\xf8\x04\xa8}\x98\xbe\x88\x1fr\xacc\x1e" + - "\x953t\xd1\x9a\xd3\x1dy\x04\xf4\xe7o:\xb7\xe5\x97" + - "\x7f\xec<\x05\xeaj\xcc\xf8\x7fw\xef[{\xaezr" + - "\xfa\x15\xfe\x04\x91\xf0\xe8" + - "R\xe3\x96_\x1c\xdf[\xf8\xf6\xaf>\"\x10\xc4&\x87" + - "}\xafo+*x9->\xdfG\x91\xb2\xe1\xe97" + - "\xbfZ=\xfa\xa3\x8f\x9b\x11\xe3\xd6{\xf2\xf2C\xa8\x9c" + - "\xe3\xab\xcf^N\xee\xfd\x9b[\xfe\xf2\x0f\xff\xf6\x8f\xfe" + - "\xf4\x13P\xafF)\xb1\xfc\x94(\xa1@\x0e~\x05\xe7" + - "\x96\xd3W\x90\xe9\xf6\xbf\x7fl\xec\xc1mO\x7f\x9a\x86" + - "\xab\xb3\xffE\x1e\xa4\xfd\xa4\xe7\xce\xa3\xfb\xdd\xb1G\xee" + - "\xf7[x\xf3\x9a\x9b\xfa\x87Q)\xf6\xd3\xcd\xa3\xfd{" + - "a\xb5\xefz\xa6\xc9\x0c\xbb\x9e)\xffv\xf4\xb1|m" + - "Y\xab\x9b\xf5\x81\xd1Y\xddqusf\x92\xcb\x0b%" + - "\xcb\xd0\xcbs%D\xb5\x8b\x94\x92\xfb\x07\x00\x10\xe5K" + - "\xb6\x02\xa0 \xcb\xc3\x00\x05}\xc6\xb4l\xe6Wt\xa7" + - "l\x99&\x03\xb1\xec\x1e\xd8\xa1\x19\x9aYf\xf1Em" + - "\x0b/\x1ac\x86a\xddl\xd9Fe\xb3\xad\xcf\xe8\xe6" + - "\x88eN\xeb3\x00%\xc4x\x9b\xb4p\xdb\x88\xa13" + - "\xd3\x9d`\xf6\x1e\xbd\xcc\xae\xf5\x1c\x16\xec\xf3l\xcd\xd5" + - "-\xf3\xcaq\xe6x\x86\xeb\x00\xa8\x191\x03\x90A\x00" + - "\xb9{\x00@\xed\x10Q\xed\x11\xb0`\xf3\x05\x98KB" + - "\x1a\x10s\x90\xdc\xd9\xbe\xf0\xce\x00\x0b\xba\x93\xd9\xd7z" + - "\xa6\xcdft\xc7ev \xbe\xb2P\xd2l\xad\xe6\xa4" + - "/<\x0e\xa0\xe6DT\xaf\x10\xd0\x9f\xb1\xb52+1" + - "\x1bu\xab\xb2I3\xad\x09\x91\x95\xb1\x0d\x04lK]" + - "\xda\xc2\x10\xeb4\xdd`\x95\xe0u\xd7\x96\xf3\xfc\xaf\x9a" + - "\x133]\xbe\xcf/\xd1\xb6\x02\xa8\xdbET\x0d\x01\xbb" + - "\xf1S\xbf\x87\x12\xa5\xac\xef\x03P\xab\"\xaa\xae\x80\xdd" + - "\xc2y\xbf\x87[m\xf7J\x00\xd5\x10Q\x9d\x15\xb0[" + - "\xfc\xc4\xef\xa1\x0c#{;\x01TWD\xf5\xa0\x80\xbe" + - "\xe3\xd5\x09S\x07D\xcb\xc6\\\xe2\xf6!:\xac2C" + - "H\x9bP`e\x02\x1as\x11\x91\x07\x0b\xa4\x8aU\xc5" + - "\\\x92y\xc2m6\xdb\xc3l\x87\x95 k[\xb3s" + - "\x98K(\xbd\x09u\xb1\x85\xa5\xe9\xff\xb1\xc2\xe4di" + - "j|\x03y`\x0a\xe0\x95\x89E%\xcf6\xb0\x0b\x04" + - "\xecJ\x1d\xd7\xfdy\x8d\x18\xf9M\xbck\xf1\xfd\xdc\xd3" + - "\xcb\xee\x95\xa5\xfc\x02\xdb\x93Y\xbaDT/\x13\xd0\xaf" + - "\xd3\xb7\xcce \xda\x0e\xe6\x92\x02\xa1\xe9\xf1m\x9f\xf1" + - "\xf8\x91\xe0\x96Rx\x8a\xed\xf0\xe8P{\xe2\xcbn\xa7" + - "\xcb\xf6\x8b\xa8\xde#\xa0\x8c\x18\xb8\xc0]6\x80z\xa7" + - "\x88\xeaa\x01Q\x08\x1c\xe0\xfeS\x00\xeaa\x11\xd5G" + - "\x05\x94E!\xb0\xff\xb1U\x00\xea\xc3\"\xaa\xcf\x0a(" + - "g\xc4\x1e\xaa\x9c\xe4\xd3\xe4\xbb\xcf\x8a\xa8\xbe$\xa0o" + - "\x05\x91I\xfa\xbb\xd8\x0d\x02v\x03\xfae\xc3\xf2*\xd3" + - "\x86\x06y\x9bU\x8akc\xb9\xe9\xd5J6\xdb\xa3\xa3" + - "\xe59C\xae\xcbjR\xddu\xb0\x1d\x04l\x07\xcc\xba" + - "\xda\x8c\x83\xcb\x00K\"b.\xc9\xbd\x80$\x8c\xcfD" + - "\x9bU\xb60\xdb\xd1E\xcb\\`\xd4\x160\x8d\x87\xfe" + - "E\xde\x15\x86\x8ae\xeb\xd2\x8cn\xaa]b\xe6\x0a\xdf" + - "\x0f1\x19\xa5\xa7\x0e\x8a\xa8n\x10\xb0\x1f?%1\xc1" + - "R\x1c\x07P\xc7DT'\x05\xec\x17\xce\x93\x98\x80Q" + - "\x09\xd6\x92\x88\xea6\x01\xb3U\xd7\xadc.\xa1\xe7\xd0" + - "v{\xd9\x0e\xc7*\xefb\x80D&1\xf1\x87\xdfV" + - "Cr\x03\xd1\xa8`.\xa9\x8a/\xc2\xeb\xb9\xcd\x0b\xee" + - "\xa8m[6\xe7\xdd\xd8\xda\xa3_N\x1e\x11\x19\xbb\xb8" + - "5y\x81,\x0c\x06\xcfRw$\xfa\xe7\xcb\x9a\xe7\xb0" + - "\x18K\x9b\xb9\xf6\xdc\xd0\xb4\x0b\"\xb3c\x16r\xaa\x96" + - "gT\xc6\x19H\xae=\x87\x08\x02\xe2\xe2\xdc\xb4\xd6\x1a" + - "KA\x1exeJO\xd2i\xad\x88j)\xd1s#" + - "\xc96\x88\xa8\xdeBz\x86\xf0O\x11\xfc\x93\"\xaau" + - "\x01}\x83\xc2\xd1\x1c\xb3@t\xdcX\xdd@X\xb2\xb8" + - "\x03J \xa0\x04\xe8{u\xc7\xb5\x99V\x03\x8c=\x8a" + - "\xd6/\xfb\x1c$\xde\x14\xfd%-\xcb\xc3\xb8\xf5\x1b\xe2" + - "\xc8\xda\xb8>\xfd\x880\xb4\xa6\x86\x13\xb0[\x07L\xd5" + - "r\\S\xab1\x00\x88\x1ev\xc0\xaa\x13\x8b\x12)\xc4" + - "Uk\x93o|\xfe\xdc\x17\xe4\xa1\x86\xccw*\x95\x88" + - "\xca\xe1n\xe4\xdbG,S\x9a\xd6g0\x97\x94yM" + - "\x0a\xb4\xb0\xfb\x90\xe7V\x99\xe9\xeae~\xe1\x02\xbb\xaf" + - "L\xfc3\xc6\xac\xf8\xe5\x14\x90\x11f\x1bw$@J" + - "\xbb\xd8\\\x04K\x9e\xd54=a\xf3\x10\xcd!\x90\xbe" + - "\x9e\xacY\xb4x\x09\xb3T\x90\xa3\x0a\x01<\xa4d." + - "VR\x9b\x07P+\x81\xcf\xc5J\xd6\x1e\x02P\xeb\"" + - "\xaa\xfbSJ\xce\x0d'\xe9Q\x16\xc5\x80\x1an'D" + - "\x0f\x8a\xa8\xde'p\xc6\x1b\x1b\x1a\xb1L\x0c/t\x00" + - "\"\xbe\xf3\xabL\xb3\xdd\x1dLC\xb7h\xba\xcc\xde\xa3" + - "\xa1\x11\xc5\xdb\x01W\xaf1\xcbs\xe3\xf8\xabi\xb3<" + - "\xdbce,\xd8%i\xae\x83\x9d `\xe7\xe2\xefm" + - "\xa4\xbfl\xf4\xdaT\x82\xd8\x97$\x08\xfa\x974\x9d\xf2" + - "]\x03 \xf0H$\xd6\xaf\x0d'e\x03\xcf\x0fmT" + - "5<\x94\x02\x80\xf2C;\x9dx<\x05@\xa0\xcf\x98" + - "\x05\x85\xc0\xc3#\x1b\x15\x02\xcb\x1d \xb6\xd1Y\xf2\x94" + - "0m\xeah\x99\x93\x1c\x03L@([\xb5\xba\xcd\x1c" + - "\x07u\xcbT=\xcd\xd0Ew\xee\xe20\xa0P\x0eB" + - "`s=\xcf\xed@ \\\x17\x81\xa0\x0c\xe1z\x80\x89" + - "A\x14qb\x03&VW\x8a8\x0c0\xb1\x96\xe4%" + - "L\x0c\xafl\xc4>\x80\x891\x92O\xa2\x80\x18\x98^" + - "Q\xf1)\x80\x89I\x12o\xc7$c*\xb7\xf1\xe3\xb7" + - "\x91\xbcJ\xf2\xb6\x0c\x87Oa\xb8\x0a`b;\xc9\xf7" + - "\x93\xbc]\xe0\x08*s\xb8\x13`b\x96\xe4w\x92\\" + - "j\xeb\xa1r^\xb9\x03m\x80\x89\x83$\xbf\x8f\xe4\x1d" + - "\x97\xf5`\x07\x80r/\x97\xdfC\xf2\x87I\xde\xd9\xdb" + - "\x83\x9d\x00\xca\x11<\x040q\x98\xe4\x8f\x92|\x09\xf6" + - "\xe0\x12\x00\xe5\x18\x1e\x07\x98x\x94\xe4\xdf%\xf9\xd2\xf6" + - "\x1e\\\x0a\xa0<\xce\xf59A\xf2\xa71\xe6\x83b%" + - "MK\xe4Nz\x92zE\xcb\x89\xc3\x8e\x85\x8d\x01\x06" + - "\x9cY\xb2\xb2\xd4\x19`6\x19\x0e\x01b\x16\xd0\xaf[" + - "\x96\xb1\xa9\x91\xee.\x94\xfdC\xb7\x80\xace\x16+q" + - "\x08\x05N\xb4\xc1\x82|Y3\x8a\xf5X\x13\xdd\x19\xf2" + - "\\\xcb\xabC\xbe\xa2\xb9\xac\x12',\xdb3\xd7\xd9V" + - "m\x12\x99]\xd3M\xcd\x80\xf8\x9b\xc5|+\xebyz" + - "e\x01\xb9\x08\xcd\x8e\x96\xaf\x0fLj<\xba:\xe2\xe8" + - "\xba\x8a\xaa\x8a+ET\xafKq\xc9j\"\xbc/\x89" + - "\xa8~E\xc0l:(\xf2{4\xc3c\x17S\xd5L" + - "51{P\x9c\x06t\x9b\xba}8\xb9=\xbe\x9cj" + - "\xbfkDT\xc7\x04<\xe0x\xe52=:Ba:" + - "\xec( Og\xa7\xec\x11O\x11B{\\l\x16\x9d" + - "an\xf0\xa9hN[\x94~$\xad\xe6\xfc\x1fw\x8f" + - "3'K\x15\xf8\x05\xfb\xb6x.p\xe1t569" + - "YJ\x9aK1 G\xce\x0b\x98\xea\xbe\x95!\xdc\x0a" + - "\x02\xb7\x1fE\xffj\x1e\x9e\xd7P\x98\xdc\xc8Y!\x17" + - "\x84\xff\x0d<\x0c\xbfB\xf2A\x0cY\x92\xc2\xff\x0f\xf0" + - "T\x03\xbbd\xe4 \xfc\x8b8\x9ef\x11\xb9\x0d\x83\xf0" + - "W\xf9\xf9%\x92o\x8bh\x81\xc2\xff\x1b8\xdf@#" + - "\x92\x18\x84?\xe3\xe1\\%\xb9\xcbi!\x13\x84\xffn" + - "|\x0e`\xc2%\xf9AN\x0bmA\xf8\xdf\x8e/6" + - "\xd0\xc8\x920\xfc\xef\xe5\xeb\xef#\xf9#\x9c\x16\x96\xf7" + - "`\x17\x80r\x94\xd3\xc8\xc3$?\x81q\x093T\x01" + - "\xb1b\xfbn\xb9\xfeu\xc6\xeaC\x905\xf4=,\xe6" + - "\xea\x8a\xae\x19k=\xcd\x80\xfc\x84\xab\x95w%%\xa3" + - "\xe1\x8cif\xc5\xc1\xaa\xb6\x8b\x11\xc3K\xe94\xe7\x1a" + - "\xce\x16f\xeb\xd3\x80I\x91\x19\xa7\xf8l\xc9\xb2\x9a3" + - "?\xafU\x98\x1d\x90I\xfc]M\x9b-V\x0c6\x82" + - "Q\xa2\x17\xcd$\xc3\xe8\xf4\x8de\x9a\x18d\xe4I=" + - "\xdf\x98j\xeba\xd9\x1a\xa5\xec\xc9BS.f\xb3u" + - "VvG,4]\xdd\xf4\xd8\x82\x03\xcaU\xcf\xdc\xc5" + - "*\xa3h\x96\xad\x8an\xce\xc0\x82zY\xfc\xac^>" + - "U\x8ft\x84N\x18\x8f\xb1\xe5\xab\x06B\x1f\xa4t," + - "\x0f$Md\xa1\xccw\x15l\xa69-\x9a\"\xf1\xb3" + - "\xa2\xac\x10\x04WP\xfd\xb4\x01\xc4s^\x8cFo\xf2" + - "\xee} \xc8\xba\x84\xc9\xc4\x12\xa3\x01\xa5|\x9b\x0d\x82" + - "<%\xa1\x10\xcf\xe21\x1a\x8d\xcb\xc5y\x10\xe4Q\x09" + - "\xc5x&\x8e\xd1|J\xbei\x18\x04y\xb5\xe4G\x15" + - "6\x14\x02u\x06\xd1\x8f\x02\x1e\xf2<\xe4\x07\xd1\x8f\xda" + - "p\x8c*q\x80A<\x10\xa6\x83AL\x8f\x82\xc4\xcf" + - "*\x87S\xa8\xa6\xea\x1e\xe2\xc6Y\x11\xd5;\x13n\xbc" + - "c>\xe9\x8b\xe3\x16\xe4\xfe\xa7Z5\xc6\x87\x00\xd4G" + - "DT\x9fO5\xc6g\xa8\xf2{^D\xf5\xa7B\x92" + - "'#\xb7\x8b\xa6'h\xd9QO\xb4\xc8\x10%t\xce" + - "\xb0bk\x1e\xa5\xf8\x15\xab\xca+:\x0c\x8er a" + - "\xea\xf4|eYj\xbe\x82Q7&5\x10{z\xda" + - "\xb2lq\xael\xe8- \x18\xb7\x90\xd7D\xc3\x7f\x8c" + - "~\xb3\x91e\xb2~\xb7\xe4G\xfd\x07Fi\x8a\x8c\x97" + - "6\xd9\xe7l\xc2\xc6Y\xde\xb9\x98\x0c\x10\x0d{/\xdc" + - "K\x07\xf7d\xc9\xd9\x9a\xe6G;SC\x1a\xc3\x0a\xdb" + - "\x99\xec\xa6T\xb6^\x0c\xab@\xe1\xa8\xf0\xcc\xd2\xe6&" + - "\xf7[\x99\xb8_\\\x18\xdc\xb125\xac\x89\x9a\x8c\xbb" + - "\xd6\x87Ny\".4\xe5\xc7\xc8QO\x88\xa8>\x9d" + - "r\xbf'i\xe1w\x03\x9f\x94\x98mGz6\x8c\xbf" + - "\x0ckf\x83n2\x87J\xaf\xa6\xce\xb8\xce\xec\x9af" + - "2\x13]\"#\xcf&Fmd\xae\xe2\xdaT\xc5\xb6" + - "\x18\xacS\xa6>[\xd2D\xb7\xda\x04\xea\xaa\xc4X\xd9" + - "\xba\xe6V/\x06\xca\x890p\x82\xb8\x09St\xaa\xa5" + - "<\x95\x1aoD@\xaa/\x86c\x83\xed) o\xa3" + - "\x96r\x9b\x88jU@_\xf3\\k\xaa^\xd1\xd0e" + - "\xebl\xb6\xdbc\x92Y\x9eK\xda-\xeaJ\xca\xce\x14" + - "\xd6\xa9~\\g\xb3\xc2n\x8f\xa5\x17D#[\x90t" + - "\xab\xb2`V\xdb\xa2`\xbb\x99\xed\x98\xb0\xca\xbb\x98\xdb" + - "0\xcanj\x8b\xb9\xe4w" + - "\x99\x0b\x8fG[\x0ec\xc7\x0b\xec\xa2\x08$\xf9\xad\xe4" + - "\xc2%d8M\x08+\xf0\xa6\x02|e\xab\xf2\x7fk" + - "X\x81\xdf\x18\xc6i.\xf9\xf16\xbc\xce\x09\xab^\x10" + - "\xa7\xad\x85\x05\xed\xff\x06\x00\x00\xff\xff\xa8\xfcvR" +const schema_db8274f9144abc7e = "x\xda\xacY{p\\\xe5u?\xe7\xde]]\xc9\x96" + + "\xbc{\xb9K\x1d\xc9\xd6l\xeb\x92I0\x98\xe2(\xb4" + + "\xa06Y\xadd9Z\xc7\x8f\xbdz\x180f\xc6\xd7" + + "\xbb\x9f\xa4k\xef\xde\xbb\xbe\x0f[rMl\\(\xa0" + + "\x1a0\x04\xcd\x80CR\xdb\xad\x0b\xa1P0!\xd3\x09" + + "%\x994}\x904\xd3!\x99\x86Ni\x93?\x1a\xf0" + + "t\x86\x96\xa1&x\x18:\x98\xdb9\xdf}j\xb5\xc8" + + "v\xa7\xfe\xc3\xda9\xfb=\xce\xf7;\xe7\xfc\xceco" + + "\xccv\x0c\x08\xeb\xd3\xafv\x01\xa8'\xd3m\xde\xef\xd5" + + "_;\xfd\xdb\xf3?\xbe\x07\xe4\x1e\xc1\xfb\xca+\x9br" + + "\x1f:G\xff\x0d\x00\xfb~\xd4v\x10\x95_\xb4I\x00" + + "\xca\x1bm\xdb\x00\xbd\x7f\xba\xf1\xd0[\xbb~\xf5\xc8\xfd" + + " \xf7`\xbc2%\x01\xf4\x9do\x9bC\xa5C\x92@" + + "\xf4\xbe~G\xee\x1f\xf0\xe4\x07\x8f\x80\xfcY\x04H#" + + "}}\xaem\x99\x00\xa8\\l+\x00z\xaf]\xff\xca" + + "\xcb\xc7\xbfu\xdf\xd7@\xfd\x0c\"\xf8\xfb{\xa5\xffA" + + "@e\xbdD\x0b\xce\xff\xe9u\xa9\xe7^\xbb\xea\x1b|" + + "\x81w\xe6\xa7\xb7\xbex\xfc[\xbf\xfe6L\x08\x12\xa6" + + "\x00\xfan\x97,Z\xcb\xa4\xff\x00\xf4\x1e\xfa\xc1J\xb3" + + "\xf8\x9f\xbbN-\xd4\xc9\xbfu\xb8\xbd\x1f\x95\x89vz" + + "\x80\xdaN\x07?\xf6/\xdf\xddZ\x7f\xe4\xc4i\x90?" + + "\x13^\xbc\xaf]\x10 \xe5\xdd\xf4\xaf\xe7\xb6myq" + + "\xf2)\xff\x1b\x7f;k\x7f\x91\xeeq\xf9\xd6\x1f\x1e\xc8" + + "\x1e+\xfe\xce\xc3O\x81\xda\x83\xc9\x8b\xf8!O\xb4\xcf" + + "\xa1r\x96.\xea{\xae=\x8f\x80\xde\xdc-\xdf\xdd\xfe" + + "\xab?\xb4\x9f\x01u\x1d\xa6\xbc\xbf}\xe0\xcd\xfd\xd7~" + + "s\xf2U\xfe\x04\x91\xf0\xe88MG_\xe8x\x1e\xd0" + + "\xeb\xfa\xab\xb5[\x1f~k\xf3Y:Zh~\xc3\xfc" + + "\xb2~T\xce,\xa37\x9cZF\xab\x7fr\xfd\xf6\xef" + + "}\xef\x85\xa9\xb3\xcd\x8a\x08\xb4\xba\xb8|\x13*\x13\xcb" + + "\xf9\x8b\x97\xd3\xea\xabK\xf8\xf3\xef\xafO\xfde\xf0." + + "\x91\x16\xa5;\xdf\xa6\xcb\xbb;i\xc1\x1d\x1f}\xfb\x07" + + "\xc3\xef\xfe\xec;Ik}\xa7S k\xfdc'=" + + "\xbc\xf7\x9d\xc1.\xe3\xdd\xa3\xdfo\x02\x98\x9ft\xa1s" + + "\x13*\x1d]t]\xba\xeby\xc0\x0f\x9e\xb9\xefx\xe9" + + "\xcd\x0d\xaf\xaa=\x98j~\xc8\xa9\xae\x83\xa8|\x9b\xd6" + + "\xf6\x9d\xed\xe2\x18E\xa84-\xe7/\xf9\xf7\x15{P" + + "\xb9\xb0\x82\xfb\xd6\x0a\xbe|\xd3\x1d_}4}\xee\xab" + + "\xaf6\xc3$\xd1\x9a\x0f3\x16*]Y\xfa\xd8\x91}" + + "J\x00\xf4z^\xf8\xdd\xbf\x18\xac\xbe\xf1\xe3&\xbd\xe9" + + "p\xe5\xc2U\xef)\xa8\xd0\xa7\x8bW\x1d\x00\xf4\xee\xbb" + + "n\xf6\xe0\xd6O\xcf\xbd\xde\x8c)W\xfcve\x0e\x95" + + "}|u]\xa1\xd5\xc29\xad\xfb\xc8?\x7f\xf1\xe7\x09" + + "/\xfa\x85\xf2K\x84\x94\xb7u\xfb\x1d{:\xeez\xf3" + + "\xcd\xa4\x17\xfdT\xe1h\x9fS\x08\xcc\x97\xe4G\x95W" + + "N\xfd\xd9[t\x91\xd4\x8cf:\xb7\x03\x95\xee\x1c}" + + "\xbc:\xc7\xdf\x10\xb9~+[_\xf8\xb5~T\xd2+" + + "I/\\Iz\xdd\xb4\xab\xc8v\xde|\xdb\xdb \xf7" + + "\x88\x0b\x02\xb9\xb8\xb2\x1f\x15\x95V\xf6mY)\xa1r" + + "\x91>z\x0fM\xed\xf8\xd1\xf9\xa1S\xff\xdd\xd2\xa3\xcf" + + "\xd1\x96\x0b|\xcb\xf9\x95\x1c\xfe\xbe\xf5\x7f\xf4\xce\xfc\x9f" + + "\x0c\x9d_t\xfa\x17\xba\x07Q\xd9\xd2Mz\x94\xba\xbf" + + "\xa4\xccv\xf3\xc3\xbf\xb2a\xdb-k\xfe\xfa\xbd$\x12" + + "Z\xf7{<\x9e\xba\x09\x89\xc9\x9b\xff\xebK\x9f~\xe8" + + "\xef\xdfk\x15\xb7\xf3\xddkQ9\xc3Oe\x98\x16\xf3\xaa\xba]1\x0d" + + "\x83\x81Xq\x0e\xef\xd6j\x9aQa\xd1E\xe9\xc5\x17" + + "\x8d\xb0Z\xcd\xbc\xd5\xb4j\xd5m\x96>\xa5\x1bC\xa6" + + "1\xa9O\x01\x94\x11\xa3m\xd2\xe2mC5\x9d\x19\xce" + + "\x18\xb3\xf6\xeb\x15v\x83k3\x7f\x9fki\x8en\x1a" + + "\xd7\x8c2\xdb\xad96\x80\x9a\x12S\x00)\x04\x90\xbb" + + "\xfa\x01\xd4v\x11\xd5\x9c\x80\x05\x8b/\xc0l\x1c\xd2\x80" + + "\x98\x85\xf8\xce\xb6\xc5w\xfaX\xd0\x9d\xcc\xba\xc15," + + "6\xa5\xdb\x0e\xb3|\xf15\x85\xb2fiu;y\xe1" + + "\x09\x005+\xa2\xbaZ@o\xca\xd2*\xac\xcc,\xd4" + + "\xcd\xeaV\xcd0\xc7DV\xc14\x08\x98N\\\xda\xc2" + + "\x10\x1b5\xbd\xc6\xaa\xfe\xebn\xa8\xe4\xf9_5+\xa6" + + ":=\x8f_\xa2\xed\x00Pw\x89\xa8\xd6\x04\xec\xc2\x8f" + + "\xbd\x1c\xa5JY?\x08\xa0N\x8b\xa8:\x02v\x09\x17" + + "\xbd\x1c\xb7\xda\xbe5\x00jMDuF\xc0.\xf1#" + + "/G9Fv\xf7\x00\xa8\x8e\x88\xea\x11\x01=\xdbm" + + "\x10\xa66\x88\xa6\x85\xd9\xd8\xed\x03tXu\x8a\x906" + + "\xa0\xc0*\x044fC*\xf7\x17HUs\x1a\xb3q" + + "\xee\x09\xb6Yl?\xb3lV\x86\x8ce\xce\xccb6" + + "\xa6\xf4&\xd4\xc5\x16\x96\xa6\xffG\x0a\xe3\xe3\xe5\x89\xd1" + + "\xcd\xe4\x81\x09\x80\xd7\xc4\x16\x95\\\xab\x86\x9d `g" + + "\xe2\xb8\xae+5b\xe87\xd1\xae\xa5\xf7sO\xaf8" + + "\xd7\x94\xf3\x8blOf\xe9\x14Q\xfd\x94\x80^\x83\xbe" + + "e\x0e\x03\xd1\xb21\x1b\x97\x08M\x8fO\x7f\xc2\xe3\x87" + + "\xfc[\xca\xc1)\x96\xcd\xa3C\xcdE\x97\xddE\x97\x1d" + + "\x12Q\xbd_@\x19\xd1w\x81{-\x00\xf5\x1e\x11\xd5" + + "\xe3\x02\xa2\xe0;\xc0\x83\xa7\x01\xd4\xe3\"\xaaO\x0a(" + + "\x8b\x82o\xff'\xd6\x02\xa8\x8f\x89\xa8\xbe \xa0\x9c\x12" + + "sT;\xc9\xcf\x91\xef\xbe \xa2\xfa\x8a\x80\x9e\xe9G" + + "&\xe9\xef`\x17\x08\xd8\x05\xe8Uj\xa6[\x9d\xaci" + + "\x90\xb7X\xb5\xb4!\x92\x1bn\xbdl\xb1\xfd:\x9a\xae" + + "]t\x1cV\x97\x1a\x8e\x8dm `\x1b`\xc6\xd1\xa6" + + "l\\\x01X\x16\x11\xb3q\xee\x05$at&Z\xac" + + "\xba\x9dY\xb6.\x9a\xc6\"\xa3\xb6\x80i4\xf0/\xf2" + + "\xae TLK\x97\xa6tC\xed\x14S\xab=/\xc0" + + "d\x98\x9e: \xa2\xbaY\xc0^\xfc\x98\xc4\x04Ki" + + "\x14@\x1d\x11Q\x1d\x17\xb0W\xb8Hb\x02F%X" + + "\xcb\"\xaa;\x05\xccL;N\x03\xb31=\x07\xb6;" + + "\xc0v\xdbfe/\x03$2\x89\x88?\xf8v: " + + "7\x10kU\xcc\xc6u\xf1ex=\xb7y\xc1\x19\xb6" + + ",\xd3\xe2\xbc\x1bY{\xf8s\xf1#Bc\x97v\xc4" + + "/\x90\x85\x01\xffY\xea\xeeX\xff|Esm\x16a" + + "i1\xc7\x9a-N: 2+b!{\xdatk" + + "\xd5Q\x06\x92c\xcd\"\x82\x80\xb847m0G\x12" + + "\x90\xfb^\x99\xd0\x93t\xda \xa2Z\x8e\xf5\xdcB\xb2" + + "\xcd\"\xaa\xb7\x91\x9e\x01\xfc\x13\x04\xff\xb8\x88jC@" + + "\xafF\xe1h\x8c\x98 \xdaN\xa4\xae/,\x9b\xdc\x01" + + "%\x10P\x02\xf4\xdc\x86\xedXL\xab\x03F\x1eE\xeb" + + "W\\\x01\x897E\x7fY\xcb\xf00n\xfd\x86(\xb2" + + "\xb6lJ>\"\x08\xad\x89\xc1\x18\xec\xd6\x013m\xda" + + "\x8e\xa1\xd5\x19\x00\x84\x0f;l6\x88E\x89\x14\xa2\xaa" + + "\xb5\xc97\xae<\xf7\xf9yhA\xe6;\x9dHD\x95" + + "`7\xf2\xedC\xa6!M\xeaS\x98\x8d\xcb\xbc&\x05" + + "Z\xd8\xbd\xe8:\xd3\xccp\xf4\x0a\xbfp\x91\xdd\xd7\xc4" + + "\xfe\x19aV\xfa\\\x02\xc8\x10\xb3-\xbbc \xa5\xbd" + + "l6\x84%\xcf\xea\x9a\x1e\xb3y\x80f\x11\xa4/\xc7" + + "k\x96,^\x82,\xe5\xe7\xa8\x82\x0fO\x13e\xce\x01" + + "\xa8GDT\x8f%\x94|\xe0Q\x00\xf5\x98\x88\xea\xe3" + + "\x09%\xe7\x07\x93\x9c)\x06\x9cI\x88>)\xa2\xfa\xb4" + + "\x80\x98\xf2)\xf3\x0cQ\xe6\xd3\"\xaa/\x09\x9c\x05G" + + "\x8aC\xa6\x81\x81\x126@\xc8\x81\xde4\xd3,g7" + + "\xd3\xd0)\x19\x0e\xb3\xf6kX\x0bc\xf0\xb0\xa3\xd7\x99" + + "\xe9:QL\xd6\xb5\x19^\x01`u\xc4\xdf%i\x8e" + + "\x8d\x1d `\x07\x85\x80\xcd\xac!\x8bU\x91\xac\xa1\xd5" + + "\xca\x9a\xe8L_\x0e@\x0b\xf92\xd3\x02\x9e\x83qF" + + "\xa1\x7fq\x9f*\xdf\xdb\x0f\x02\x0f]zs}0\xae" + + "3xBIS\x99\xf1h\\P\xf0\x84\xd2F'\x9e" + + "\x88\x01\x0fT\x1b1\xa1\xe0\x87D\xa8s\xc17\xf5a" + + "\xa2'\x9d\xc5\xef\x0c\xf2\xac\x8e\xa61\xce\x01\xc2\x18\xa1" + + "\x8aYoX\xcc\xb6Q7\x0d\xd5\xd5j\xba\xe8\xccF" + + "\x1b\x97\xc4\x80b\xdf\x8f\x99m\x8d<7\x12\x81pc" + + "\x08\x82R\xc4M\x00c\x03(\xe2\xd8f\x8c\xddD)" + + "\xe1 \xc0\xd8\x06\x92\x971\xf6\x14e\x0b\xf6\x00\x8c\x8d" + + "\x90|\x1c\x05D\xdfW\x14\x15\x9f\x01\x18\x1b'\xf1." + + "\x8cS\xacr'?~'\xc9\xa7I\x9eNq\xf8\x14" + + "\x86k\x01\xc6v\x91\xfc\x10\xc9\xdb\x04\x8e\xa02\x8b{" + + "\x00\xc6fH~\x0f\xc9\xa5t\x8e\xea\x7f\xe5n\xb4\x00" + + "\xc6\x8e\x90\xfc\x18\xc9\xdb?\x95\xc3v\xaa\xf1\xb9\xfc~" + + "\x92?F\xf2\x8e\xee\x1cv\x00(\x8f\xe0Q\x80\xb1\xe3" + + "$\x7f\x92\xe4\xcb0\x87\xcb\x00\x94'\xf0\x04\xc0\xd8\x93" + + "$\x7f\x9a\xe4\xcb\xdbr\xb8\x1c@9\xc3\xf59I\xf2" + + "g1\"\x90R5\xc9c\xe4Nz\x9c\xabE\xd3\x8e" + + "\xdc\x90\x05\x9d\x04\xfa$[63\xd4J`&\x9e'" + + "\x01b\x06\xd0k\x98fm\xebB~\xbcT\xb9\x10\xb8" + + "\x05dL\xa3T\x8d\xe2\xcbw\xa2\xcd&\xe4+Z\xad" + + "\xd4\x884\xd1\xed\xa2\xeb\x98n\x03\xf2U\xcda\xd5(" + + "\xc3Y\xae\xb1\xd12\xeb\xe3\xc8\xac\xbanh5\x88\xbe" + + "Y\xca\xb72\xae\xabW\x17\x05\x9b\xd0\xech\xf9F\xff" + + "\xb8\xc6\xa3\xab=\x8a\xaek\xa9\x0c\xb9FD\xf5\xc6\x04" + + "\xf9\xac#\x86\xfc\xac\x88\xea\xe7\x05\xcc$\x83\"\xbf_" + + "\xab\xb9\xecr\xca\xa0\x89\xa6T\xe0W\xb3>?'n" + + "\x1f\x8co\x8f.\xa7b\xf1z\x11\xd5\x11\x01\x0f\xdbn" + + "\xa5B\x8f\x0eQ\x98\x0cZ\x10\xc8\xd3\xd9\x09{Dc" + + "\x87\xc0\x1e\x97\x9bv\xa7\x98\xe3\x7f*\x19\x93&\xe5+" + + "I\xab\xdb\xff\xc7\xdd\xa3\xcc\xceP\xc9~\xc9F/\x1a" + + "$\\:\xbf\x8d\x8c\x8f\x97\xe3nT\xf4\xc9\x91\xf3\x02" + + "&\xdau\xa5\x88;@\xe0\xf6\xa3\xe8_\xc7\xc3\xf3z" + + "\x0a\x93\x9b9+d\xfd\xf0\xbf\x89\x87\xe1\xe7I>\x80" + + "\x01KR\xf8\x7f\x01O/`\x97\x94\xec\x87\x7f\x09G" + + "\x93,\"\xa7\xd1\x0f\x7f\x95\x9f_&\xf9\xce\x90\x16(" + + "\xfco\xc7\xb9\x054\"\x89~\xf83\x1e\xce\xd3$w" + + "8-\xa4\xfc\xf0\xdf\x87/\x02\x8c9$?\xc2i!" + + "\xed\x87\xff]\xf8\xf2\x02\x1aY\x16\x84\xff\x03|\xfd1" + + "\x92?\xcei\xe1\xaa\x1cv\x02(\xf3\x9cF\x1e#\xf9" + + "I\x8cj\x9eb\x15\xc4\xaa\xe59\x95\xc6\x97\x19k\x14" + + "!S\xd3\xf7\xb3\x88\xab\xab\xbaV\xdb\xe0j5\xc8\x8f" + + "9Zeo\\c\xd6\xec\x11\xcd\xa8\xda8\xad\xede" + + "\xc4\xf0R2\x07:5{;\xb3\xf4I\xc0\xb8*\x8d" + + "j\x82L\xd94\x9bK\x05^\xdc0\xcb'\x93\xe8\xbb" + + "\xba6S\xaa\xd6\xd8\x10\x86\x95\x81h\xc4\x19F\xa7o" + + "L\xc3@?]\x8f\xeb\xf9\x85y\xb8\x11\xd4\xb9a>" + + "\x1f/4%j6\xd3`\x15g\xc8D\xc3\xd1\x0d\x97" + + "-:\xa02\xed\x1a{Yu\x18\x8d\x8aY\xd5\x8d)" + + "XT`\x8b\x9f\xd4\xfc'\x0a\x98\xf6\xc0\x09\xa3\xc9\xb7" + + "|m\x7f\xe0\x83\x94\x8e\xe5\xfe\xb8\xeb,T\xf8\xae\x82" + + "\xc54\xbbE\x17%~R\x94\x15\xfc\xe0\xa2\xdb\xb2b" + + "\x1a \x1a\x0dc8\xab\x93\xf7\x1d\x04A\xd6%\x8cG" + + "\x9c\x18N4\xe5;-\x10\xe4\x09\x09\x85h|\x8f\xe1" + + "4].\xcd\x81 \x0fK(Fct\x0c\x07Z\xf2" + + "-\x83 \xc8\xeb$/,\xc9\xa1\xe0\xab3\x80^\x18" + + "\xf0\x90\xe7!?\x80^\xd8\xb7cX\xba\x03\x0c\xe0\xe1" + + " \x1d\x0c`rv$~R\xfd\xdc\xba,$n\x9c" + + "\x11Q\xbd'\xe6\xc6\xbb\xe7\xe2F:\xeaY\x1e|\xa6" + + "U'}\x14@}\xdc\xaf\x00\xa3N\xfa,\x95\x8a/" + + "\x89\xa8\xfeD\x88\xf3d\xe8v\xe1\xb8\x05M+l\xa2" + + "\x96\x98\xba\x04\xce\x19Tl\xcd\xb3\x17\xafjN\xf3\x8a" + + "\x0e\xfd\xa3l\x88\x99:9\x90Y\x91\x18\xc8`\xd8\xbe" + + "I\x0b\x88=9\x9eY\xb14W.hF\xc0\x9f\xcf" + + "\x90\xd7\x84\xbf\x17`\xf83\x8f,\x93\xf5\xbb$/l" + + "X0LSd\xbc\xa4\xc9\xae\xb0k\x1bey\xfbr" + + "2@8\x1d\xbet\xf3\xed\xdf\x93!gk\x1a8\xed" + + "ILujf\xd0\xffd\xb6&\xb2\xf5RX\xf9\x0a" + + "\x87\x85g\x8667\xb9\xdf\x9a\xd8\xfd\xa2\xc2\xe0\xee5" + + "\x89\xe9N\xd8\x95\xdc\xbb)p\xca\x93Q\xa1)\x7f\x9d" + + "\x1c\xf5\xa4\x88\xea\xb3\x09\xf7\xfb\xe6\xa6\xb8+\x91\x98e" + + "\x85z.\x98\x97\xd5\xcc\xa9\xcd\xba\xc1l*\xbd\x9aZ" + + "\xe9\x06\xb3\xea\x9a\xc1\x0ct\x88\x8c\\\x8b\x18u!s" + + "\x956$*\xb6\xa5`\x9d0\xf4\x19\xde\xa24\x81\xba" + + "66V\xa6\xa1]^\x073\x16\x04\x8e\x1f7A\x8a" + + "N\xf4\xa0\xa7\x13\xf3\x90\x10H\xf5\xe5`\xce\xb0+\x01" + + "\xe4\x9d\xd4\x83\xee\x14Q\x9d\x16\xd0\xd3\\\xc7\x9chT" + + "5t\xd8F\x8b\xeds\x99dTf\xe3^\x8c\xba\x92" + + "\x8a=\x81\x0d\xaa\x1f7Z\xac\xb0\xcfe\xc9\x05\xe1\x8c" + + "\x17$\xdd\xac.\x1a\xee\xb6(\xd8ne\xbb\xc7\xcc\xca" + + "^\xe6,\x98}\xfb\xd4\x1b>E[\x13+\x18\xbe\x84" + + "\x8d\x02\xa8U\x7fb\x12QR}O<\xdd\x8d(\xc9" + + "\x9d\x8b=j\xe1\x88\xf4\xff'\xa9.5\xd9_PE" + + "\xf9#\xb9)=o\x14\xabU\x8b\x12Y8\xc1NV" + + "\xc3\xf1\x04{\xdd\xdaD9\x1c\x0c\xdf\xa2\xdfp\xfd\x10" + + "\xce\xb8\x86>\x83\xd9\xf8\x87\x9cK\xcfS[NoG" + + "\x0b\xec\xb2\x08$\xfeq\xe5\xd2%d0~\x08*\xf0" + + "\xa6\x02|M\xab\xf2\x7fGP\x81\xdf\x1c\xc4i6\xfe" + + "\xbd7\xb8\xce\x0e\xaa^\x10'\xcd\xc5\x05\xed\xff\x06\x00" + + "\x00\xff\xff\xb0\x8e\x80\xdd" func init() { schemas.Register(schema_db8274f9144abc7e,