TUN-1961: Create EdgeConnectionManager to maintain outbound connections to the edge

This commit is contained in:
Chung-Ting Huang 2019-06-17 16:18:47 -05:00
parent d26a8c5d44
commit 80a15547e3
15 changed files with 856 additions and 569 deletions

View File

@ -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)
}

View File

@ -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"))

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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},

281
connection/manager.go Normal file
View File

@ -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
}

View File

@ -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())
}

View File

@ -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)
}

View File

@ -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,
}
}

View File

@ -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}
}()
}

View File

@ -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,

View File

@ -72,6 +72,7 @@ type EdgeConnectionConfig struct {
HeartbeatInterval time.Duration
Timeout time.Duration
MaxFailedHeartbeats uint64
UserCredentialPath string
}
// FailReason impelents FallibleConfig interface for EdgeConnectionConfig

View File

@ -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 {

View File

@ -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<EG\xff\xba\xf3\x19@\xbf" +
"\xfboVmz\xf0\xed\x0dg\xe8h\xa1\xf9\x0dG\x97" +
"\x0c\xa0\xf2\xf8\x12z\xc3\xc9%\xb4\xfa\xa7\xd7l\xf9\xfe" +
"\xf7\x9f\x9d9\xd3\xac\x88@\xab\x87\x96\xaeGej)" +
"\x7f\xf1RZ}I\x11\x7f\xfe\x83\xeb3\x7f\x1d\xbeK" +
"\xa4Em]\xef\xd2\xe5\xbd]\xb4\xe0\xd6O^\xf8\xe1" +
"\xe8\xfb?;\x9b\xb6\xd6\xd9.\x81\xac\xf5\x93.zx" +
"\xff{\xc3\xdd\xe6\xfb\x87~\xd0\x040?\xe9\xd7]\xeb" +
"Q\xe9\xec\xa6\xeb\xda\xba\x9f\x01\xfc\xe8\xa9\xbb\x0f\x17\xdf" +
"Z\xfb\x8a\xda\x87\x99&\x079\xd9\xbd\x0f\x95\x17\xf8\xd2" +
"3\xddd\xb8\x18\x93\xc6\xc5\xc1;\x1e[\xb6\x13\x953" +
"\xcb8\xa0\xcb8\xa0\xebo\xfd\xd6Cm\xef|\xeb\x95" +
"f\x90$Z\xf3B\xd6F\xe5\xd5,}\xfc\xfb\xec\x13" +
"\x02\xa0\xdf\xf7\xec\xef\xfd\xd5p\xe5\xcd\x1f7i-\xf0" +
"\xfb\x97\x7f\xa8\x9c[N\x9f\xce.\xdf\x0b\xe8\xdf}\xf5" +
"\xdc\xbeM_\x98\x7f\xa3\x19Q\x8e\xc5%\xca<*\xab" +
"\x15Z}\x95B\xab\x85w\xb4\xde\x83\xff\xfc\xd5\x9f\xa7" +
"|\xe8\x98\xf2\x0b\x84\x8c\xbfi\xcb\xad;;o\x7f\xeb" +
"\xad\xb4\x0f\xdd\xafp\xacO*\x04\xe5\xf3\xf2C\xcaK" +
"'\xff\xe2m\xbaHj\xc6\xf2ee+*o\xd0E" +
"k^W\xf8\x1bb\xc7oe\xe93\x97\x0e\xa0\xf2\xf2" +
"\xa5\xa4\xd7\xb9KI\xaf\x1b\xb6\x0f\xb1m7\xde\xf2." +
"\xc8}bC\x18\x9f\xa7\x95\xdd+hS\xe7\x0a\x09\x95" +
"\xb3\xf4\xd1\x7f`f\xeb\xab\x1f\x8c\x9c\xfc\xef\x96\xfe|" +
"r\xc5\x00*g\xf8\x96\xd3+8\xfck\xae\xff\x93\xf7" +
"\x8e\xfe\xd9\xc8\x07\x0bN\xff\xf8\xb2aT:{\xb9\x0b" +
"\xf4~M\xb9\xa1\x97\x1f\xfe\xcd\xb5\x9boZ\xf9\xf2\x87" +
"i$\xfa{?\xe4\x11\xdeKHL\xdf\xf8__\xfb" +
"\xc2\x03\xff\xf0a\xab\xa8U{W\xa1\xa2\xf1\x13o\xa3" +
"\xc5\xef\xaf\xfb\xce\xcf\xfa\xb2}\xbfj\xa5\xe8\x1d\xbd;" +
"Q9Jk\xd7\x1c\xe9\xe5\x8a\x1a\xbf\xf5\xc6\xaeg\xda" +
"\xdf\xfe\xa8\xd5\xc9/\xf7\xf5\xa1\xf2z\x1f\x9d\xfc\x93>" +
"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<W&\x0aF/a\xe3\xa9f4\xa2" +
"\xa4\xda\xcedX\x1bS\x927\x9fxT\xe3\xc4\xf3\xff" +
"'\xa9.6\xa8o\xa8\xa2\x82\x09\xdb\x8c\x9e7\x87*" +
"\x15\x9b\x12Y4\x90NW\xc3\xc9@z\xf5\xaaT9" +
"\x1c\xce\xd2\xe2\x1fd\x83\x10\xcez\xa6>\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\xc3O<E\x8b\xdf" +
"\xdd\xf8\x8d\x9f\xf5dz\xdeo\xa5\xe8\xdfu\xefA\xe5" +
"\x0dZ\xdb\xf7z7W\xb4\xf6\x9b\xaf\xef}\xbe\xed\xad" +
"\x0fZ\x9d\x9c^\xd5\x83\xca\xd5\xab\xe8dy\x15\xa9q" +
"\xdb/O\x1c(|\xed\xfd\x0f\x08\x04\xb1\x89\xd3\xd6\xaf" +
"\xda\x81\xca0_\\\\E\xb1\xb2\xf9\xd97\xbe8=" +
"\xff\xc3\x0f\x9b\x11\xe3\xd6{g\xd5QTp5w\xaa" +
"U\xc4L\xbf\xb1\xfd\xcf\x7f\xffo\xfe\xe0\x8f?\x02\xf5" +
":\x94b\xcbO\x88\x12\x0a\xe4\xe0\xab9\xbb\x9c_M" +
"\xa6;\xf4\xee\x13#\x0f\xef|\xf6\xe3$\\[z_" +
"\xa6\x05w\xf6\x92\x9e{\xe6\x0f9#\x8f?\xe8\xb5\xf0" +
"\xe6\xbe\xbbz\x07Qy\xb0\x97n~\xa0\xf7\x00\xac\xf3" +
"\x1c\xd70X\xcdj\xa4*\xbf\x15~\xac\xdcP\xd1\x1a" +
"F\xa3\x7fxF\xb7\x1d\xdd\x98\x1a\xe7\xf2B\xd9\xac\xe9" +
"\x95\xd92\xa2\xdaIJ\xc9\xbd\xfd\x00\x88\xf2\xd5;\x00" +
"P\x90\xe5A\x80\x82>e\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,