From 1d96bccc04a8a40aea2184d271b6dd9facfa5611 Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Wed, 13 Nov 2019 14:04:19 -0600 Subject: [PATCH 01/21] TUN-2178: public API to create new h2mux.MuxedStreamRequest --- h2mux/h2mux.go | 4 ++-- h2mux/muxwriter.go | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/h2mux/h2mux.go b/h2mux/h2mux.go index 347ad87b..59f722cc 100644 --- a/h2mux/h2mux.go +++ b/h2mux/h2mux.go @@ -390,7 +390,7 @@ func isConnectionClosedError(err error) bool { // Called by proxy server and tunnel func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader) (*MuxedStream, error) { stream := m.NewStream(headers) - if err := m.MakeMuxedStreamRequest(ctx, MuxedStreamRequest{stream, body}); err != nil { + if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, body)); err != nil { return nil, err } if err := m.AwaitResponseHeaders(ctx, stream); err != nil { @@ -401,7 +401,7 @@ func (m *Muxer) OpenStream(ctx context.Context, headers []Header, body io.Reader func (m *Muxer) OpenRPCStream(ctx context.Context) (*MuxedStream, error) { stream := m.NewStream(RPCHeaders()) - if err := m.MakeMuxedStreamRequest(ctx, MuxedStreamRequest{stream: stream, body: nil}); err != nil { + if err := m.MakeMuxedStreamRequest(ctx, NewMuxedStreamRequest(stream, nil)); err != nil { return nil, err } if err := m.AwaitResponseHeaders(ctx, stream); err != nil { diff --git a/h2mux/muxwriter.go b/h2mux/muxwriter.go index b0769356..80888d45 100644 --- a/h2mux/muxwriter.go +++ b/h2mux/muxwriter.go @@ -54,6 +54,13 @@ type MuxedStreamRequest struct { body io.Reader } +func NewMuxedStreamRequest(stream *MuxedStream, body io.Reader) MuxedStreamRequest { + return MuxedStreamRequest{ + stream: stream, + body: body, + } +} + func (r *MuxedStreamRequest) flushBody() { io.Copy(r.stream, r.body) r.stream.CloseWrite() From 6ea9b5c3ffa4d18c44238153938717ba54a1d421 Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Wed, 13 Nov 2019 09:11:35 -0600 Subject: [PATCH 02/21] TUN-2490: respect original representation of HTTP request path --- streamhandler/request.go | 39 ++- streamhandler/request_test.go | 441 ++++++++++++++++++++++++++++++++++ validation/validation.go | 8 + validation/validation_test.go | 135 ++++------- 4 files changed, 531 insertions(+), 92 deletions(-) create mode 100644 streamhandler/request_test.go diff --git a/streamhandler/request.go b/streamhandler/request.go index 40e36d2b..f82c7c7d 100644 --- a/streamhandler/request.go +++ b/streamhandler/request.go @@ -35,26 +35,49 @@ func createRequest(stream *h2mux.MuxedStream, url *url.URL) (*http.Request, erro return req, nil } +// H2RequestHeadersToH1Request converts the HTTP/2 headers to an HTTP/1 Request +// object. This includes conversion of the pseudo-headers into their closest +// HTTP/1 equivalents. See https://tools.ietf.org/html/rfc7540#section-8.1.2.3 func H2RequestHeadersToH1Request(h2 []h2mux.Header, h1 *http.Request) error { for _, header := range h2 { switch header.Name { case ":method": h1.Method = header.Value case ":scheme": + // noop - use the preexisting scheme from h1.URL case ":authority": // Otherwise the host header will be based on the origin URL h1.Host = header.Value case ":path": - u, err := url.Parse(header.Value) + // We don't want to be an "opinionated" proxy, so ideally we would use :path as-is. + // However, this HTTP/1 Request object belongs to the Go standard library, + // whose URL package makes some opinionated decisions about the encoding of + // URL characters: see the docs of https://godoc.org/net/url#URL, + // in particular the EscapedPath method https://godoc.org/net/url#URL.EscapedPath, + // which is always used when computing url.URL.String(), whether we'd like it or not. + // + // Well, not *always*. We could circumvent this by using url.URL.Opaque. But + // that would present unusual difficulties when using an HTTP proxy: url.URL.Opaque + // is treated differently when HTTP_PROXY is set! + // See https://github.com/golang/go/issues/5684#issuecomment-66080888 + // + // This means we are subject to the behavior of net/url's function `shouldEscape` + // (as invoked with mode=encodePath): https://github.com/golang/go/blob/go1.12.7/src/net/url/url.go#L101 + + if header.Value == "*" { + h1.URL.Path = "*" + continue + } + // Due to the behavior of validation.ValidateUrl, h1.URL may + // already have a partial value, with or without a trailing slash. + base := h1.URL.String() + base = strings.TrimRight(base, "/") + // But we know :path begins with '/', because we handled '*' above - see RFC7540 + url, err := url.Parse(base + header.Value) if err != nil { - return fmt.Errorf("unparseable path") + return errors.Wrap(err, fmt.Sprintf("invalid path '%v'", header.Value)) } - resolved := h1.URL.ResolveReference(u) - // prevent escaping base URL - if !strings.HasPrefix(resolved.String(), h1.URL.String()) { - return fmt.Errorf("invalid path %s", header.Value) - } - h1.URL = resolved + h1.URL = url case "content-length": contentLength, err := strconv.ParseInt(header.Value, 10, 64) if err != nil { diff --git a/streamhandler/request_test.go b/streamhandler/request_test.go new file mode 100644 index 00000000..d3a85f76 --- /dev/null +++ b/streamhandler/request_test.go @@ -0,0 +1,441 @@ +package streamhandler + +import ( + "fmt" + "math/rand" + "net/http" + "net/url" + "reflect" + "regexp" + "strings" + "testing" + "testing/quick" + + "github.com/cloudflare/cloudflared/h2mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestH2RequestHeadersToH1Request_RegularHeaders(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) + assert.NoError(t, err) + + headersConversionErr := H2RequestHeadersToH1Request( + []h2mux.Header{ + h2mux.Header{ + Name: "Mock header 1", + Value: "Mock value 1", + }, + h2mux.Header{ + Name: "Mock header 2", + Value: "Mock value 2", + }, + }, + request, + ) + + assert.Equal(t, http.Header{ + "Mock header 1": []string{"Mock value 1"}, + "Mock header 2": []string{"Mock value 2"}, + }, request.Header) + + assert.NoError(t, headersConversionErr) +} + +func TestH2RequestHeadersToH1Request_NoHeaders(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) + assert.NoError(t, err) + + headersConversionErr := H2RequestHeadersToH1Request( + []h2mux.Header{}, + request, + ) + + assert.Equal(t, http.Header{}, request.Header) + + assert.NoError(t, headersConversionErr) +} + +func TestH2RequestHeadersToH1Request_InvalidHostPath(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "http://example.com", nil) + assert.NoError(t, err) + + headersConversionErr := H2RequestHeadersToH1Request( + []h2mux.Header{ + h2mux.Header{ + Name: ":path", + Value: "//bad_path/", + }, + h2mux.Header{ + Name: "Mock header", + Value: "Mock value", + }, + }, + request, + ) + + assert.Equal(t, http.Header{ + "Mock header": []string{"Mock value"}, + }, request.Header) + + assert.Equal(t, "http://example.com//bad_path/", request.URL.String()) + + assert.NoError(t, headersConversionErr) +} + +func TestH2RequestHeadersToH1Request_HostPathWithQuery(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) + assert.NoError(t, err) + + headersConversionErr := H2RequestHeadersToH1Request( + []h2mux.Header{ + h2mux.Header{ + Name: ":path", + Value: "/?query=mock%20value", + }, + h2mux.Header{ + Name: "Mock header", + Value: "Mock value", + }, + }, + request, + ) + + assert.Equal(t, http.Header{ + "Mock header": []string{"Mock value"}, + }, request.Header) + + assert.Equal(t, "http://example.com/?query=mock%20value", request.URL.String()) + + assert.NoError(t, headersConversionErr) +} + +func TestH2RequestHeadersToH1Request_HostPathWithURLEncoding(t *testing.T) { + request, err := http.NewRequest(http.MethodGet, "http://example.com/", nil) + assert.NoError(t, err) + + headersConversionErr := H2RequestHeadersToH1Request( + []h2mux.Header{ + h2mux.Header{ + Name: ":path", + Value: "/mock%20path", + }, + h2mux.Header{ + Name: "Mock header", + Value: "Mock value", + }, + }, + request, + ) + + assert.Equal(t, http.Header{ + "Mock header": []string{"Mock value"}, + }, request.Header) + + assert.Equal(t, "http://example.com/mock%20path", request.URL.String()) + + assert.NoError(t, headersConversionErr) +} + +func TestH2RequestHeadersToH1Request_WeirdURLs(t *testing.T) { + type testCase struct { + path string + want string + } + testCases := []testCase{ + { + path: "", + want: "", + }, + { + path: "/", + want: "/", + }, + { + path: "//", + want: "//", + }, + { + path: "/test", + want: "/test", + }, + { + path: "//test", + want: "//test", + }, + { + // https://github.com/cloudflare/cloudflared/issues/81 + path: "//test/", + want: "//test/", + }, + { + path: "/%2Ftest", + want: "/%2Ftest", + }, + { + path: "//%20test", + want: "//%20test", + }, + { + // https://github.com/cloudflare/cloudflared/issues/124 + path: "/test?get=somthing%20a", + want: "/test?get=somthing%20a", + }, + { + path: "/%20", + want: "/%20", + }, + { + // stdlib's EscapedPath() will always percent-encode ' ' + path: "/ ", + want: "/%20", + }, + { + path: "/ a ", + want: "/%20a%20", + }, + { + path: "/a%20b", + want: "/a%20b", + }, + { + path: "/foo/bar;param?query#frag", + want: "/foo/bar;param?query#frag", + }, + { + // stdlib's EscapedPath() will always percent-encode non-ASCII chars + path: "/a␠b", + want: "/a%E2%90%A0b", + }, + { + path: "/a-umlaut-ä", + want: "/a-umlaut-%C3%A4", + }, + { + path: "/a-umlaut-%C3%A4", + want: "/a-umlaut-%C3%A4", + }, + { + path: "/a-umlaut-%c3%a4", + want: "/a-umlaut-%c3%a4", + }, + { + // here the second '#' is treated as part of the fragment + path: "/a#b#c", + want: "/a#b%23c", + }, + { + path: "/a#b␠c", + want: "/a#b%E2%90%A0c", + }, + { + path: "/a#b%20c", + want: "/a#b%20c", + }, + { + path: "/a#b c", + want: "/a#b%20c", + }, + { + // stdlib's EscapedPath() will always percent-encode '\' + path: "/\\", + want: "/%5C", + }, + { + path: "/a\\", + want: "/a%5C", + }, + { + path: "/a,b.c.", + want: "/a,b.c.", + }, + { + path: "/.", + want: "/.", + }, + { + // stdlib's EscapedPath() will always percent-encode '`' + path: "/a`", + want: "/a%60", + }, + { + path: "/a[0]", + want: "/a[0]", + }, + { + path: "/?a[0]=5 &b[]=", + want: "/?a[0]=5 &b[]=", + }, + { + path: "/?a=%22b%20%22", + want: "/?a=%22b%20%22", + }, + } + + for index, testCase := range testCases { + requestURL := "https://example.com" + + request, err := http.NewRequest(http.MethodGet, requestURL, nil) + assert.NoError(t, err) + headersConversionErr := H2RequestHeadersToH1Request( + []h2mux.Header{ + h2mux.Header{ + Name: ":path", + Value: testCase.path, + }, + h2mux.Header{ + Name: "Mock header", + Value: "Mock value", + }, + }, + request, + ) + assert.NoError(t, headersConversionErr) + + assert.Equal(t, + http.Header{ + "Mock header": []string{"Mock value"}, + }, + request.Header) + + assert.Equal(t, + "https://example.com"+testCase.want, + request.URL.String(), + "Failed URL index: %v %#v", index, testCase) + } +} + +func TestH2RequestHeadersToH1Request_QuickCheck(t *testing.T) { + config := &quick.Config{ + Values: func(args []reflect.Value, rand *rand.Rand) { + args[0] = reflect.ValueOf(randomHTTP2Path(t, rand)) + }, + } + + type testOrigin struct { + url string + + expectedScheme string + expectedBasePath string + } + testOrigins := []testOrigin{ + { + url: "http://origin.hostname.example.com:8080", + expectedScheme: "http", + expectedBasePath: "http://origin.hostname.example.com:8080", + }, + { + url: "http://origin.hostname.example.com:8080/", + expectedScheme: "http", + expectedBasePath: "http://origin.hostname.example.com:8080", + }, + { + url: "http://origin.hostname.example.com:8080/api", + expectedScheme: "http", + expectedBasePath: "http://origin.hostname.example.com:8080/api", + }, + { + url: "http://origin.hostname.example.com:8080/api/", + expectedScheme: "http", + expectedBasePath: "http://origin.hostname.example.com:8080/api", + }, + { + url: "https://origin.hostname.example.com:8080/api", + expectedScheme: "https", + expectedBasePath: "https://origin.hostname.example.com:8080/api", + }, + } + + // use multiple schemes to demonstrate that the URL is based on the + // origin's scheme, not the :scheme header + for _, testScheme := range []string{"http", "https"} { + for _, testOrigin := range testOrigins { + assertion := func(testPath string) bool { + const expectedMethod = "POST" + const expectedHostname = "request.hostname.example.com" + + h2 := []h2mux.Header{ + h2mux.Header{Name: ":method", Value: expectedMethod}, + h2mux.Header{Name: ":scheme", Value: testScheme}, + h2mux.Header{Name: ":authority", Value: expectedHostname}, + h2mux.Header{Name: ":path", Value: testPath}, + } + h1, err := http.NewRequest("GET", testOrigin.url, nil) + require.NoError(t, err) + + err = H2RequestHeadersToH1Request(h2, h1) + return assert.NoError(t, err) && + assert.Equal(t, expectedMethod, h1.Method) && + assert.Equal(t, expectedHostname, h1.Host) && + assert.Equal(t, testOrigin.expectedScheme, h1.URL.Scheme) && + assert.Equal(t, testOrigin.expectedBasePath+testPath, h1.URL.String()) + } + err := quick.Check(assertion, config) + assert.NoError(t, err) + } + } +} + +func randomASCIIPrintableChar(rand *rand.Rand) int { + // smallest printable ASCII char is 32, largest is 126 + const startPrintable = 32 + const endPrintable = 127 + return startPrintable + rand.Intn(endPrintable-startPrintable) +} + +// randomASCIIText generates an ASCII string, some of whose characters may be +// percent-encoded. Its "logical length" (ignoring percent-encoding) is +// between 1 and `maxLength`. +func randomASCIIText(rand *rand.Rand, minLength int, maxLength int) string { + length := minLength + rand.Intn(maxLength) + result := "" + for i := 0; i < length; i++ { + c := randomASCIIPrintableChar(rand) + + // 1/4 chance of using percent encoding when not necessary + if c == '%' || rand.Intn(4) == 0 { + result += fmt.Sprintf("%%%02X", c) + } else { + result += string(c) + } + } + return result +} + +// Calls `randomASCIIText` and ensures the result is a valid URL path, +// i.e. one that can pass unchanged through url.URL.String() +func randomHTTP1Path(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string { + text := randomASCIIText(rand, minLength, maxLength) + regexp, err := regexp.Compile("[^/;,]*") + require.NoError(t, err) + return "/" + regexp.ReplaceAllStringFunc(text, url.PathEscape) +} + +// Calls `randomASCIIText` and ensures the result is a valid URL query, +// i.e. one that can pass unchanged through url.URL.String() +func randomHTTP1Query(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string { + text := randomASCIIText(rand, minLength, maxLength) + return "?" + strings.ReplaceAll(text, "#", "%23") +} + +// Calls `randomASCIIText` and ensures the result is a valid URL fragment, +// i.e. one that can pass unchanged through url.URL.String() +func randomHTTP1Fragment(t *testing.T, rand *rand.Rand, minLength int, maxLength int) string { + text := randomASCIIText(rand, minLength, maxLength) + url, err := url.Parse("#" + text) + require.NoError(t, err) + return url.String() +} + +// Assemble a random :path pseudoheader that is legal by Go stdlib standards +// (i.e. all characters will satisfy "net/url".shouldEscape for their respective locations) +func randomHTTP2Path(t *testing.T, rand *rand.Rand) string { + result := randomHTTP1Path(t, rand, 1, 64) + if rand.Intn(2) == 1 { + result += randomHTTP1Query(t, rand, 1, 32) + } + if rand.Intn(2) == 1 { + result += randomHTTP1Fragment(t, rand, 1, 16) + } + return result +} diff --git a/validation/validation.go b/validation/validation.go index 7d7589d1..83857230 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -60,6 +60,12 @@ func ValidateHostname(hostname string) (string, error) { } +// ValidateUrl returns a validated version of `originUrl` with a scheme prepended (by default http://). +// Note: when originUrl contains a scheme, the path is removed: +// ValidateUrl("https://localhost:8080/api/") => "https://localhost:8080" +// but when it does not, the path is preserved: +// ValidateUrl("localhost:8080/api/") => "http://localhost:8080/api/" +// This is arguably a bug, but changing it might break some cloudflared users. func ValidateUrl(originUrl string) (string, error) { if originUrl == "" { return "", fmt.Errorf("URL should not be empty") @@ -121,6 +127,8 @@ func ValidateUrl(originUrl string) (string, error) { if err != nil { return "", fmt.Errorf("URL %s has invalid format", originUrl) } + // This is why the path is preserved when `originUrl` doesn't have a schema. + // Using `parsedUrl.Port()` here, instead of `port`, would remove the path return fmt.Sprintf("%s://%s", defaultScheme, net.JoinHostPort(hostname, port)), nil } } diff --git a/validation/validation_test.go b/validation/validation_test.go index 6a7ec48b..866414ad 100644 --- a/validation/validation_test.go +++ b/validation/validation_test.go @@ -53,98 +53,65 @@ func TestValidateHostname(t *testing.T) { } func TestValidateUrl(t *testing.T) { + type testCase struct { + input string + expectedOutput string + } + testCases := []testCase{ + {"http://localhost", "http://localhost"}, + {"http://localhost/", "http://localhost"}, + {"http://localhost/api", "http://localhost"}, + {"http://localhost/api/", "http://localhost"}, + {"https://localhost", "https://localhost"}, + {"https://localhost/", "https://localhost"}, + {"https://localhost/api", "https://localhost"}, + {"https://localhost/api/", "https://localhost"}, + {"https://localhost:8080", "https://localhost:8080"}, + {"https://localhost:8080/", "https://localhost:8080"}, + {"https://localhost:8080/api", "https://localhost:8080"}, + {"https://localhost:8080/api/", "https://localhost:8080"}, + {"localhost", "http://localhost"}, + {"localhost/", "http://localhost/"}, + {"localhost/api", "http://localhost/api"}, + {"localhost/api/", "http://localhost/api/"}, + {"localhost:8080", "http://localhost:8080"}, + {"localhost:8080/", "http://localhost:8080/"}, + {"localhost:8080/api", "http://localhost:8080/api"}, + {"localhost:8080/api/", "http://localhost:8080/api/"}, + {"localhost:8080/api/?asdf", "http://localhost:8080/api/?asdf"}, + {"http://127.0.0.1:8080", "http://127.0.0.1:8080"}, + {"127.0.0.1:8080", "http://127.0.0.1:8080"}, + {"127.0.0.1", "http://127.0.0.1"}, + {"https://127.0.0.1:8080", "https://127.0.0.1:8080"}, + {"[::1]:8080", "http://[::1]:8080"}, + {"http://[::1]", "http://[::1]"}, + {"http://[::1]:8080", "http://[::1]:8080"}, + {"[::1]", "http://[::1]"}, + {"https://example.com", "https://example.com"}, + {"example.com", "http://example.com"}, + {"http://hello.example.com", "http://hello.example.com"}, + {"hello.example.com", "http://hello.example.com"}, + {"hello.example.com:8080", "http://hello.example.com:8080"}, + {"https://hello.example.com:8080", "https://hello.example.com:8080"}, + {"https://bücher.example.com", "https://xn--bcher-kva.example.com"}, + {"bücher.example.com", "http://xn--bcher-kva.example.com"}, + {"https%3A%2F%2Fhello.example.com", "https://hello.example.com"}, + {"https://alex:12345@hello.example.com:8080", "https://hello.example.com:8080"}, + } + for i, testCase := range testCases { + validUrl, err := ValidateUrl(testCase.input) + assert.NoError(t, err, "test case %v", i) + assert.Equal(t, testCase.expectedOutput, validUrl, "test case %v", i) + } + validUrl, err := ValidateUrl("") assert.Equal(t, fmt.Errorf("URL should not be empty"), err) assert.Empty(t, validUrl) - validUrl, err = ValidateUrl("https://localhost:8080") - assert.Nil(t, err) - assert.Equal(t, "https://localhost:8080", validUrl) - - validUrl, err = ValidateUrl("localhost:8080") - assert.Nil(t, err) - assert.Equal(t, "http://localhost:8080", validUrl) - - validUrl, err = ValidateUrl("http://localhost") - assert.Nil(t, err) - assert.Equal(t, "http://localhost", validUrl) - - validUrl, err = ValidateUrl("http://127.0.0.1:8080") - assert.Nil(t, err) - assert.Equal(t, "http://127.0.0.1:8080", validUrl) - - validUrl, err = ValidateUrl("127.0.0.1:8080") - assert.Nil(t, err) - assert.Equal(t, "http://127.0.0.1:8080", validUrl) - - validUrl, err = ValidateUrl("127.0.0.1") - assert.Nil(t, err) - assert.Equal(t, "http://127.0.0.1", validUrl) - - validUrl, err = ValidateUrl("https://127.0.0.1:8080") - assert.Nil(t, err) - assert.Equal(t, "https://127.0.0.1:8080", validUrl) - - validUrl, err = ValidateUrl("[::1]:8080") - assert.Nil(t, err) - assert.Equal(t, "http://[::1]:8080", validUrl) - - validUrl, err = ValidateUrl("http://[::1]") - assert.Nil(t, err) - assert.Equal(t, "http://[::1]", validUrl) - - validUrl, err = ValidateUrl("http://[::1]:8080") - assert.Nil(t, err) - assert.Equal(t, "http://[::1]:8080", validUrl) - - validUrl, err = ValidateUrl("[::1]") - assert.Nil(t, err) - assert.Equal(t, "http://[::1]", validUrl) - - validUrl, err = ValidateUrl("https://example.com") - assert.Nil(t, err) - assert.Equal(t, "https://example.com", validUrl) - - validUrl, err = ValidateUrl("example.com") - assert.Nil(t, err) - assert.Equal(t, "http://example.com", validUrl) - - validUrl, err = ValidateUrl("http://hello.example.com") - assert.Nil(t, err) - assert.Equal(t, "http://hello.example.com", validUrl) - - validUrl, err = ValidateUrl("hello.example.com") - assert.Nil(t, err) - assert.Equal(t, "http://hello.example.com", validUrl) - - validUrl, err = ValidateUrl("hello.example.com:8080") - assert.Nil(t, err) - assert.Equal(t, "http://hello.example.com:8080", validUrl) - - validUrl, err = ValidateUrl("https://hello.example.com:8080") - assert.Nil(t, err) - assert.Equal(t, "https://hello.example.com:8080", validUrl) - - validUrl, err = ValidateUrl("https://bücher.example.com") - assert.Nil(t, err) - assert.Equal(t, "https://xn--bcher-kva.example.com", validUrl) - - validUrl, err = ValidateUrl("bücher.example.com") - assert.Nil(t, err) - assert.Equal(t, "http://xn--bcher-kva.example.com", validUrl) - - validUrl, err = ValidateUrl("https%3A%2F%2Fhello.example.com") - assert.Nil(t, err) - assert.Equal(t, "https://hello.example.com", validUrl) - validUrl, err = ValidateUrl("ftp://alex:12345@hello.example.com:8080/robot.txt") assert.Equal(t, "Currently Argo Tunnel does not support ftp protocol.", err.Error()) assert.Empty(t, validUrl) - validUrl, err = ValidateUrl("https://alex:12345@hello.example.com:8080") - assert.Nil(t, err) - assert.Equal(t, "https://hello.example.com:8080", validUrl) - } func TestToggleProtocol(t *testing.T) { From ca7fbf43daf64e6e640432745349f31d440bb605 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Mon, 18 Nov 2019 10:28:18 -0600 Subject: [PATCH 03/21] TUN-2547: TunnelRPC definitions for Authenticate flow --- go.mod | 1 - tunnelrpc/pogs/auth_outcome.go | 83 ++++ tunnelrpc/pogs/auth_serialize.go | 78 +++ tunnelrpc/pogs/auth_test.go | 109 ++++ tunnelrpc/pogs/tunnelrpc.go | 1 + tunnelrpc/tunnelrpc.capnp | 8 + tunnelrpc/tunnelrpc.capnp.go | 820 ++++++++++++++++++++++--------- 7 files changed, 876 insertions(+), 224 deletions(-) create mode 100644 tunnelrpc/pogs/auth_outcome.go create mode 100644 tunnelrpc/pogs/auth_serialize.go create mode 100644 tunnelrpc/pogs/auth_test.go diff --git a/go.mod b/go.mod index bce9d655..c55ae1b9 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,6 @@ require ( golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sys v0.0.0-20191008105621-543471e840be golang.org/x/text v0.3.2 // indirect - google.golang.org/appengine v1.4.0 // indirect google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect google.golang.org/grpc v1.24.0 // indirect gopkg.in/coreos/go-oidc.v2 v2.1.0 diff --git a/tunnelrpc/pogs/auth_outcome.go b/tunnelrpc/pogs/auth_outcome.go new file mode 100644 index 00000000..9f72711e --- /dev/null +++ b/tunnelrpc/pogs/auth_outcome.go @@ -0,0 +1,83 @@ +package pogs + +import ( + "fmt" + "time" +) + +// AuthenticateResponse is the serialized response from the Authenticate RPC. +// It's a 1:1 representation of the capnp message, so it's not very useful for programmers. +// Instead, you should call the `Outcome()` method to get a programmer-friendly sum type, with one +// case for each possible outcome. +type AuthenticateResponse struct { + PermanentErr string + RetryableErr string + Jwt []byte + HoursUntilRefresh uint8 +} + +// Outcome turns the deserialized response of Authenticate into a programmer-friendly sum type. +func (ar AuthenticateResponse) Outcome() AuthOutcome { + // If there was a network error, then cloudflared should retry later, + // because origintunneld couldn't prove whether auth was correct or not. + if ar.RetryableErr != "" { + return &AuthUnknown{Err: fmt.Errorf(ar.RetryableErr), HoursUntilRefresh: ar.HoursUntilRefresh} + } + + // If the user's authentication was unsuccessful, the server will return an error explaining why. + // cloudflared should fatal with this error. + if ar.PermanentErr != "" { + return &AuthFail{Err: fmt.Errorf(ar.PermanentErr)} + } + + // If auth succeeded, return the token and refresh it when instructed. + if ar.PermanentErr == "" && len(ar.Jwt) > 0 { + return &AuthSuccess{Jwt: ar.Jwt, HoursUntilRefresh: ar.HoursUntilRefresh} + } + + // Otherwise the state got messed up. + return nil +} + +// AuthOutcome is a programmer-friendly sum type denoting the possible outcomes of Authenticate. +//go-sumtype:decl AuthOutcome +type AuthOutcome interface { + isAuthOutcome() +} + +// AuthSuccess means the backend successfully authenticated this cloudflared. +type AuthSuccess struct { + Jwt []byte + HoursUntilRefresh uint8 +} + +// RefreshAfter is how long cloudflared should wait before rerunning Authenticate. +func (ao *AuthSuccess) RefreshAfter() time.Duration { + return hoursToTime(ao.HoursUntilRefresh) +} + +func (ao *AuthSuccess) isAuthOutcome() {} + +// AuthFail means this cloudflared has the wrong auth and should exit. +type AuthFail struct { + Err error +} + +func (ao *AuthFail) isAuthOutcome() {} + +// AuthUnknown means the backend couldn't finish checking authentication. Try again later. +type AuthUnknown struct { + Err error + HoursUntilRefresh uint8 +} + +// RefreshAfter is how long cloudflared should wait before rerunning Authenticate. +func (ao *AuthUnknown) RefreshAfter() time.Duration { + return hoursToTime(ao.HoursUntilRefresh) +} + +func (ao *AuthUnknown) isAuthOutcome() {} + +func hoursToTime(hours uint8) time.Duration { + return time.Duration(hours) * time.Hour +} diff --git a/tunnelrpc/pogs/auth_serialize.go b/tunnelrpc/pogs/auth_serialize.go new file mode 100644 index 00000000..b6caa7c3 --- /dev/null +++ b/tunnelrpc/pogs/auth_serialize.go @@ -0,0 +1,78 @@ +package pogs + +import ( + "context" + + "github.com/cloudflare/cloudflared/tunnelrpc" + + "zombiezen.com/go/capnproto2/pogs" + "zombiezen.com/go/capnproto2/server" +) + +func (i TunnelServer_PogsImpl) Authenticate(p tunnelrpc.TunnelServer_authenticate) error { + originCert, err := p.Params.OriginCert() + if err != nil { + return err + } + hostname, err := p.Params.Hostname() + if err != nil { + return err + } + options, err := p.Params.Options() + if err != nil { + return err + } + pogsOptions, err := UnmarshalRegistrationOptions(options) + if err != nil { + return err + } + + server.Ack(p.Options) + resp, err := i.impl.Authenticate(p.Ctx, originCert, hostname, pogsOptions) + if err != nil { + return err + } + result, err := p.Results.NewResult() + if err != nil { + return err + } + return MarshalAuthenticateResponse(result, resp) +} + +func MarshalAuthenticateResponse(s tunnelrpc.AuthenticateResponse, p *AuthenticateResponse) error { + return pogs.Insert(tunnelrpc.AuthenticateResponse_TypeID, s.Struct, p) +} + +func (c TunnelServer_PogsClient) Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error) { + client := tunnelrpc.TunnelServer{Client: c.Client} + promise := client.Authenticate(ctx, func(p tunnelrpc.TunnelServer_authenticate_Params) error { + err := p.SetOriginCert(originCert) + if err != nil { + return err + } + err = p.SetHostname(hostname) + if err != nil { + return err + } + registrationOptions, err := p.NewOptions() + if err != nil { + return err + } + err = MarshalRegistrationOptions(registrationOptions, options) + if err != nil { + return err + } + return nil + }) + retval, err := promise.Result().Struct() + if err != nil { + return nil, err + } + return UnmarshalAuthenticateResponse(retval) +} + +func UnmarshalAuthenticateResponse(s tunnelrpc.AuthenticateResponse) (*AuthenticateResponse, error) { + p := new(AuthenticateResponse) + err := pogs.Extract(p, tunnelrpc.AuthenticateResponse_TypeID, s.Struct) + return p, err +} diff --git a/tunnelrpc/pogs/auth_test.go b/tunnelrpc/pogs/auth_test.go new file mode 100644 index 00000000..50ae1ec1 --- /dev/null +++ b/tunnelrpc/pogs/auth_test.go @@ -0,0 +1,109 @@ +package pogs + +import ( + "fmt" + "reflect" + "testing" + "time" + + "github.com/cloudflare/cloudflared/tunnelrpc" + "github.com/stretchr/testify/assert" + capnp "zombiezen.com/go/capnproto2" +) + +// Ensure the AuthOutcome sum is correct +var _ AuthOutcome = &AuthSuccess{} +var _ AuthOutcome = &AuthFail{} +var _ AuthOutcome = &AuthUnknown{} + +// Unit tests for AuthenticateResponse.Outcome() +func TestAuthenticateResponseOutcome(t *testing.T) { + type fields struct { + PermanentErr string + RetryableErr string + Jwt []byte + HoursUntilRefresh uint8 + } + tests := []struct { + name string + fields fields + want AuthOutcome + }{ + {"success", + fields{Jwt: []byte("asdf"), HoursUntilRefresh: 6}, + &AuthSuccess{Jwt: []byte("asdf"), HoursUntilRefresh: 6}, + }, + {"fail", + fields{PermanentErr: "bad creds"}, + &AuthFail{Err: fmt.Errorf("bad creds")}, + }, + {"error", + fields{RetryableErr: "bad conn", HoursUntilRefresh: 6}, + &AuthUnknown{Err: fmt.Errorf("bad conn"), HoursUntilRefresh: 6}, + }, + {"nil (no fields are set)", + fields{}, + nil, + }, + {"nil (too few fields are set)", + fields{HoursUntilRefresh: 6}, + nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ar := AuthenticateResponse{ + PermanentErr: tt.fields.PermanentErr, + RetryableErr: tt.fields.RetryableErr, + Jwt: tt.fields.Jwt, + HoursUntilRefresh: tt.fields.HoursUntilRefresh, + } + if got := ar.Outcome(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("AuthenticateResponse.Outcome() = %T, want %v", got, tt.want) + } + }) + } +} + +func TestWhenToRefresh(t *testing.T) { + expected := 4 * time.Hour + actual := hoursToTime(4) + if expected != actual { + t.Fatalf("expected %v hours, got %v", expected, actual) + } +} + +// Test that serializing and deserializing AuthenticationResponse undo each other. +func TestSerializeAuthenticationResponse(t *testing.T) { + + tests := []*AuthenticateResponse{ + &AuthenticateResponse{ + Jwt: []byte("\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98"), + HoursUntilRefresh: 24, + }, + &AuthenticateResponse{ + PermanentErr: "bad auth", + }, + &AuthenticateResponse{ + RetryableErr: "bad connection", + HoursUntilRefresh: 24, + }, + } + + for i, testCase := range tests { + _, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) + capnpEntity, err := tunnelrpc.NewAuthenticateResponse(seg) + if !assert.NoError(t, err) { + t.Fatal("Couldn't initialize a new message") + } + err = MarshalAuthenticateResponse(capnpEntity, testCase) + if !assert.NoError(t, err, "testCase index %v failed to marshal", i) { + continue + } + result, err := UnmarshalAuthenticateResponse(capnpEntity) + if !assert.NoError(t, err, "testCase index %v failed to unmarshal", i) { + continue + } + assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i) + } +} diff --git a/tunnelrpc/pogs/tunnelrpc.go b/tunnelrpc/pogs/tunnelrpc.go index 981d5299..0e99cffe 100644 --- a/tunnelrpc/pogs/tunnelrpc.go +++ b/tunnelrpc/pogs/tunnelrpc.go @@ -327,6 +327,7 @@ type TunnelServer interface { GetServerInfo(ctx context.Context) (*ServerInfo, error) UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error) + Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error) } func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer { diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index 5664dcc3..8cffa64d 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -274,11 +274,19 @@ struct FailedConfig { reason @4 :Text; } +struct AuthenticateResponse { + permanentErr @0 :Text; + retryableErr @1 :Text; + jwt @2 :Data; + hoursUntilRefresh @3 :UInt8; +} + interface TunnelServer { registerTunnel @0 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); getServerInfo @1 () -> (result :ServerInfo); unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> (); connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult); + authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse); } interface ClientService { diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index 34e07f10..75f03a41 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -2625,6 +2625,121 @@ func (p FailedConfig_config_Promise) ReverseProxy() ReverseProxyConfig_Promise { return ReverseProxyConfig_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } +type AuthenticateResponse struct{ capnp.Struct } + +// AuthenticateResponse_TypeID is the unique identifier for the type AuthenticateResponse. +const AuthenticateResponse_TypeID = 0x82c325a07ad22a65 + +func NewAuthenticateResponse(s *capnp.Segment) (AuthenticateResponse, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}) + return AuthenticateResponse{st}, err +} + +func NewRootAuthenticateResponse(s *capnp.Segment) (AuthenticateResponse, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}) + return AuthenticateResponse{st}, err +} + +func ReadRootAuthenticateResponse(msg *capnp.Message) (AuthenticateResponse, error) { + root, err := msg.RootPtr() + return AuthenticateResponse{root.Struct()}, err +} + +func (s AuthenticateResponse) String() string { + str, _ := text.Marshal(0x82c325a07ad22a65, s.Struct) + return str +} + +func (s AuthenticateResponse) PermanentErr() (string, error) { + p, err := s.Struct.Ptr(0) + return p.Text(), err +} + +func (s AuthenticateResponse) HasPermanentErr() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s AuthenticateResponse) PermanentErrBytes() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return p.TextBytes(), err +} + +func (s AuthenticateResponse) SetPermanentErr(v string) error { + return s.Struct.SetText(0, v) +} + +func (s AuthenticateResponse) RetryableErr() (string, error) { + p, err := s.Struct.Ptr(1) + return p.Text(), err +} + +func (s AuthenticateResponse) HasRetryableErr() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s AuthenticateResponse) RetryableErrBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err +} + +func (s AuthenticateResponse) SetRetryableErr(v string) error { + return s.Struct.SetText(1, v) +} + +func (s AuthenticateResponse) Jwt() ([]byte, error) { + p, err := s.Struct.Ptr(2) + return []byte(p.Data()), err +} + +func (s AuthenticateResponse) HasJwt() bool { + p, err := s.Struct.Ptr(2) + return p.IsValid() || err != nil +} + +func (s AuthenticateResponse) SetJwt(v []byte) error { + return s.Struct.SetData(2, v) +} + +func (s AuthenticateResponse) HoursUntilRefresh() uint8 { + return s.Struct.Uint8(0) +} + +func (s AuthenticateResponse) SetHoursUntilRefresh(v uint8) { + s.Struct.SetUint8(0, v) +} + +// AuthenticateResponse_List is a list of AuthenticateResponse. +type AuthenticateResponse_List struct{ capnp.List } + +// NewAuthenticateResponse creates a new list of AuthenticateResponse. +func NewAuthenticateResponse_List(s *capnp.Segment, sz int32) (AuthenticateResponse_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 3}, sz) + return AuthenticateResponse_List{l}, err +} + +func (s AuthenticateResponse_List) At(i int) AuthenticateResponse { + return AuthenticateResponse{s.List.Struct(i)} +} + +func (s AuthenticateResponse_List) Set(i int, v AuthenticateResponse) error { + return s.List.SetStruct(i, v.Struct) +} + +func (s AuthenticateResponse_List) String() string { + str, _ := text.MarshalList(0x82c325a07ad22a65, s.List) + return str +} + +// AuthenticateResponse_Promise is a wrapper for a AuthenticateResponse promised by a client call. +type AuthenticateResponse_Promise struct{ *capnp.Pipeline } + +func (p AuthenticateResponse_Promise) Struct() (AuthenticateResponse, error) { + s, err := p.Pipeline.Struct() + return AuthenticateResponse{s}, err +} + type TunnelServer struct{ Client capnp.Client } // TunnelServer_TypeID is the unique identifier for the type TunnelServer. @@ -2710,6 +2825,26 @@ func (c TunnelServer) Connect(ctx context.Context, params func(TunnelServer_conn } return TunnelServer_connect_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } +func (c TunnelServer) Authenticate(ctx context.Context, params func(TunnelServer_authenticate_Params) error, opts ...capnp.CallOption) TunnelServer_authenticate_Results_Promise { + if c.Client == nil { + return TunnelServer_authenticate_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} + } + call := &capnp.Call{ + Ctx: ctx, + Method: capnp.Method{ + InterfaceID: 0xea58385c65416035, + MethodID: 4, + InterfaceName: "tunnelrpc/tunnelrpc.capnp:TunnelServer", + MethodName: "authenticate", + }, + Options: capnp.NewCallOptions(opts), + } + if params != nil { + call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 3} + call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_authenticate_Params{Struct: s}) } + } + return TunnelServer_authenticate_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} +} type TunnelServer_Server interface { RegisterTunnel(TunnelServer_registerTunnel) error @@ -2719,6 +2854,8 @@ type TunnelServer_Server interface { UnregisterTunnel(TunnelServer_unregisterTunnel) error Connect(TunnelServer_connect) error + + Authenticate(TunnelServer_authenticate) error } func TunnelServer_ServerToClient(s TunnelServer_Server) TunnelServer { @@ -2728,7 +2865,7 @@ func TunnelServer_ServerToClient(s TunnelServer_Server) TunnelServer { func TunnelServer_Methods(methods []server.Method, s TunnelServer_Server) []server.Method { if cap(methods) == 0 { - methods = make([]server.Method, 0, 4) + methods = make([]server.Method, 0, 5) } methods = append(methods, server.Method{ @@ -2787,6 +2924,20 @@ func TunnelServer_Methods(methods []server.Method, s TunnelServer_Server) []serv ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) + methods = append(methods, server.Method{ + Method: capnp.Method{ + InterfaceID: 0xea58385c65416035, + MethodID: 4, + InterfaceName: "tunnelrpc/tunnelrpc.capnp:TunnelServer", + MethodName: "authenticate", + }, + Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { + call := TunnelServer_authenticate{c, opts, TunnelServer_authenticate_Params{Struct: p}, TunnelServer_authenticate_Results{Struct: r}} + return s.Authenticate(call) + }, + ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, + }) + return methods } @@ -2822,6 +2973,14 @@ type TunnelServer_connect struct { Results TunnelServer_connect_Results } +// TunnelServer_authenticate holds the arguments for a server call to TunnelServer.authenticate. +type TunnelServer_authenticate struct { + Ctx context.Context + Options capnp.CallOptions + Params TunnelServer_authenticate_Params + Results TunnelServer_authenticate_Results +} + type TunnelServer_registerTunnel_Params struct{ capnp.Struct } // TunnelServer_registerTunnel_Params_TypeID is the unique identifier for the type TunnelServer_registerTunnel_Params. @@ -3448,6 +3607,207 @@ func (p TunnelServer_connect_Results_Promise) Result() ConnectResult_Promise { return ConnectResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } +type TunnelServer_authenticate_Params struct{ capnp.Struct } + +// TunnelServer_authenticate_Params_TypeID is the unique identifier for the type TunnelServer_authenticate_Params. +const TunnelServer_authenticate_Params_TypeID = 0x85c8cea1ab1894f3 + +func NewTunnelServer_authenticate_Params(s *capnp.Segment) (TunnelServer_authenticate_Params, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) + return TunnelServer_authenticate_Params{st}, err +} + +func NewRootTunnelServer_authenticate_Params(s *capnp.Segment) (TunnelServer_authenticate_Params, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) + return TunnelServer_authenticate_Params{st}, err +} + +func ReadRootTunnelServer_authenticate_Params(msg *capnp.Message) (TunnelServer_authenticate_Params, error) { + root, err := msg.RootPtr() + return TunnelServer_authenticate_Params{root.Struct()}, err +} + +func (s TunnelServer_authenticate_Params) String() string { + str, _ := text.Marshal(0x85c8cea1ab1894f3, s.Struct) + return str +} + +func (s TunnelServer_authenticate_Params) OriginCert() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return []byte(p.Data()), err +} + +func (s TunnelServer_authenticate_Params) HasOriginCert() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s TunnelServer_authenticate_Params) SetOriginCert(v []byte) error { + return s.Struct.SetData(0, v) +} + +func (s TunnelServer_authenticate_Params) Hostname() (string, error) { + p, err := s.Struct.Ptr(1) + return p.Text(), err +} + +func (s TunnelServer_authenticate_Params) HasHostname() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s TunnelServer_authenticate_Params) HostnameBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err +} + +func (s TunnelServer_authenticate_Params) SetHostname(v string) error { + return s.Struct.SetText(1, v) +} + +func (s TunnelServer_authenticate_Params) Options() (RegistrationOptions, error) { + p, err := s.Struct.Ptr(2) + return RegistrationOptions{Struct: p.Struct()}, err +} + +func (s TunnelServer_authenticate_Params) HasOptions() bool { + p, err := s.Struct.Ptr(2) + return p.IsValid() || err != nil +} + +func (s TunnelServer_authenticate_Params) SetOptions(v RegistrationOptions) error { + return s.Struct.SetPtr(2, v.Struct.ToPtr()) +} + +// NewOptions sets the options field to a newly +// allocated RegistrationOptions struct, preferring placement in s's segment. +func (s TunnelServer_authenticate_Params) NewOptions() (RegistrationOptions, error) { + ss, err := NewRegistrationOptions(s.Struct.Segment()) + if err != nil { + return RegistrationOptions{}, err + } + err = s.Struct.SetPtr(2, ss.Struct.ToPtr()) + return ss, err +} + +// TunnelServer_authenticate_Params_List is a list of TunnelServer_authenticate_Params. +type TunnelServer_authenticate_Params_List struct{ capnp.List } + +// NewTunnelServer_authenticate_Params creates a new list of TunnelServer_authenticate_Params. +func NewTunnelServer_authenticate_Params_List(s *capnp.Segment, sz int32) (TunnelServer_authenticate_Params_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}, sz) + return TunnelServer_authenticate_Params_List{l}, err +} + +func (s TunnelServer_authenticate_Params_List) At(i int) TunnelServer_authenticate_Params { + return TunnelServer_authenticate_Params{s.List.Struct(i)} +} + +func (s TunnelServer_authenticate_Params_List) Set(i int, v TunnelServer_authenticate_Params) error { + return s.List.SetStruct(i, v.Struct) +} + +func (s TunnelServer_authenticate_Params_List) String() string { + str, _ := text.MarshalList(0x85c8cea1ab1894f3, s.List) + return str +} + +// TunnelServer_authenticate_Params_Promise is a wrapper for a TunnelServer_authenticate_Params promised by a client call. +type TunnelServer_authenticate_Params_Promise struct{ *capnp.Pipeline } + +func (p TunnelServer_authenticate_Params_Promise) Struct() (TunnelServer_authenticate_Params, error) { + s, err := p.Pipeline.Struct() + return TunnelServer_authenticate_Params{s}, err +} + +func (p TunnelServer_authenticate_Params_Promise) Options() RegistrationOptions_Promise { + return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} +} + +type TunnelServer_authenticate_Results struct{ capnp.Struct } + +// TunnelServer_authenticate_Results_TypeID is the unique identifier for the type TunnelServer_authenticate_Results. +const TunnelServer_authenticate_Results_TypeID = 0xfc5edf80e39c0796 + +func NewTunnelServer_authenticate_Results(s *capnp.Segment) (TunnelServer_authenticate_Results, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) + return TunnelServer_authenticate_Results{st}, err +} + +func NewRootTunnelServer_authenticate_Results(s *capnp.Segment) (TunnelServer_authenticate_Results, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) + return TunnelServer_authenticate_Results{st}, err +} + +func ReadRootTunnelServer_authenticate_Results(msg *capnp.Message) (TunnelServer_authenticate_Results, error) { + root, err := msg.RootPtr() + return TunnelServer_authenticate_Results{root.Struct()}, err +} + +func (s TunnelServer_authenticate_Results) String() string { + str, _ := text.Marshal(0xfc5edf80e39c0796, s.Struct) + return str +} + +func (s TunnelServer_authenticate_Results) Result() (AuthenticateResponse, error) { + p, err := s.Struct.Ptr(0) + return AuthenticateResponse{Struct: p.Struct()}, err +} + +func (s TunnelServer_authenticate_Results) HasResult() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s TunnelServer_authenticate_Results) SetResult(v AuthenticateResponse) error { + return s.Struct.SetPtr(0, v.Struct.ToPtr()) +} + +// NewResult sets the result field to a newly +// allocated AuthenticateResponse struct, preferring placement in s's segment. +func (s TunnelServer_authenticate_Results) NewResult() (AuthenticateResponse, error) { + ss, err := NewAuthenticateResponse(s.Struct.Segment()) + if err != nil { + return AuthenticateResponse{}, err + } + err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) + return ss, err +} + +// TunnelServer_authenticate_Results_List is a list of TunnelServer_authenticate_Results. +type TunnelServer_authenticate_Results_List struct{ capnp.List } + +// NewTunnelServer_authenticate_Results creates a new list of TunnelServer_authenticate_Results. +func NewTunnelServer_authenticate_Results_List(s *capnp.Segment, sz int32) (TunnelServer_authenticate_Results_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) + return TunnelServer_authenticate_Results_List{l}, err +} + +func (s TunnelServer_authenticate_Results_List) At(i int) TunnelServer_authenticate_Results { + return TunnelServer_authenticate_Results{s.List.Struct(i)} +} + +func (s TunnelServer_authenticate_Results_List) Set(i int, v TunnelServer_authenticate_Results) error { + return s.List.SetStruct(i, v.Struct) +} + +func (s TunnelServer_authenticate_Results_List) String() string { + str, _ := text.MarshalList(0xfc5edf80e39c0796, s.List) + return str +} + +// TunnelServer_authenticate_Results_Promise is a wrapper for a TunnelServer_authenticate_Results promised by a client call. +type TunnelServer_authenticate_Results_Promise struct{ *capnp.Pipeline } + +func (p TunnelServer_authenticate_Results_Promise) Struct() (TunnelServer_authenticate_Results, error) { + s, err := p.Pipeline.Struct() + return TunnelServer_authenticate_Results{s}, err +} + +func (p TunnelServer_authenticate_Results_Promise) Result() AuthenticateResponse_Promise { + return AuthenticateResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)} +} + type ClientService struct{ Client capnp.Client } // ClientService_TypeID is the unique identifier for the type ClientService. @@ -3681,233 +4041,246 @@ func (p ClientService_useConfiguration_Results_Promise) Result() UseConfiguratio return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } -const schema_db8274f9144abc7e = "x\xda\xacY{\x8c\\\xe5u?\xe7~\xb3{\xd7\x8f" + - "\xf1\xcc\xed\x1d\x843\xf5j+\x0b\x1a\xec`\x17\xe3\xd0" + - "\xe2m\xc9\xec\xc3vv7k{\xee\xce\xae\x01c$" + - "_\xcf|\xbb{\xd7w\xee\x1d\xdf\x87\xd9\xb5L\x8c-" + - "S`\x0b\xc1&\xb1\x84\x89\x89\xc0\x8d\xcbCv\x83\x89" + - "Q\x0b\x05\x14\xaa\xa6\x84&\x11q\x8b\xab\xd0\x12\x09\x02" + - "V\x15TDM\"!\xaa\x84[\x9d\xfb\xde\xd9e\x8d" + - "\xab\xfc\xb3;:s\xbe\xef;\xcf\xdfy\xccu\x99\x05" + - "]\xc2\x9a\x163\x0b\xa0\x1coi\xf5Z\xfe\xf8\xfc\xdb" + - "\x8d\xb7\xc5C \x15\xd1\xfb\xfa\x8b\x03\x85O\x9c\x83\xff" + - "\x09-\x82\x08\xb0\xf6\xd1\xd6\x01\x94\xcf\xb4\x8a\x00\xf2\xe9" + - "\xd6;\x00\xbd\xbf\xa8\xbf~\xe2O\x8f\xfe\x98\x98\x85\x84" + - "\x19p\xed\x15\xe2^\x94W\x88\xc4y\xb5\xb8\x05\xd0\xfb" + - "\xb7\xeb\xf6\xbd\xb7\xe3\xd7G\xee\x9dym\x86n\xbdI" + - "\x9cFyD\x14\x81y\x8f\xdeV\xf8\x17|\xec\xe3#" + - " ]\x83\x00-H_\xaf\x11\x17\x0a\x80r\xbfX\x02" + - "\xf4^\xbf\xf6\xc5\x17\x0e\x7f\xff\x9eo\x83\xf2ED\x08" + - "\xcek\xe2\xff\"\xa0|\xa7\xcfp\xf1\xbb_\xca\x9c~" + - "\xfd\x0f\xbe\xe33x'\xcf\xdd\xfc\xec\xe1\xef\xff\xd1\xfb" + - "0\"\x88\x98\x01X{R\xb4\x88\xf7\x8c\xf8_\x80\xde" + - "\xb7~\xfe\xd2\xe6\xfa\x91GN\x80\xf4\xc5\xe8\xae#m" + - "\x82\x00\x19\xef\x86\xff\xb8\xb0e\xd3\xb3\xa3O\x04\xdf\x04" + - "r\xdc\xdd\xf6,\x1d=\xdaF\xcf\xfc\xe8\x8e\xfc\xfd\xdd" + - "\x7f\xf6\xe0\x13\xa0\x141m\xa6\x16\xe2|\xbem\x1a\xe5" + - "sm\xf4\xf1\xa7m7#\xa07\xbd\xee\xa5\xad\xbf\xfe" + - "K\xfbiPVa\xc6\xfb\xa7\xfb\xde\xdd\xb3\xe2\xa9\xd1" + - "W}\xa9\x18\xc0\xda\x0d\x0b\xff\x95\xae\xbeu\xe1\xf7\x00" + - "\xbd\xec?\xac\xdc\xfc\xe0{\x83g\xe8\xea\x94Q\x03!" + - ">Y\xd8\x89\xf2\x82Ed\xd7\x96E\xc4\xfd\xb3k\xb7" + - "\xbe\xfc\xf23cg\x9a\x05\xf1\xfdur\xd1\x00\xca\xcf" + - "\xfb\xdc\xcf\xf9\xdcW\xf4\xe3[?X\x93\xf9\xbbP/" + - "\xe6\x9bo\xf1\xfb\xbe\xf9\x16\x13\xc3m\xbf}\xee\x1f7" + - "|\xf8\xc6\xf3i\x07\xb4g\x05r\xc0\x9a,)\xbem" + - "\x1a\xebouv\xbd\x0c\xca5\x88\xde\xc4\xd1}N\xdf" + - "\xc3\x0fx0\x82\"\x0a\x00ko\xcd\xee\xa5\xcbx\x96" + - "\xa2\xa3\xfd\x83\x9e\xac\xf1\xe1\xc1\x1f4\x85\x92\xff\xea+" + - "\xd9\x01\x94\xcfgI\xb4s\xd9\xef\x01~\xfc\xf4=\x87" + - "\xfb\xdf]\xff\xaaR\xc4L\xb3\xd2\xf5%{Q>\xb0" + - "\x84>\xde\xb9\xa4\x83\xec\x19[\xb0\x89\xdd\xd7\xfatn" + - "\x02\xe5Wr\xf4\xf1\xa5\x9c\xcf>p\xdb7\x1fj\xb9" + - "\xf0\xcdW\x9bMJ\xf1\xb9\xf6\xb5\xbc\x85\xf2/\xf2\xf4" + - "\xf1\xcd\xfc\x13\x02\xa0W|\xe6\xcf\xff\xb6\xa7\xf6\xe6\x8f" + - "\xe7H\x01\xf9\x87\xf2G\xf29\x99>\xfdT&\x1d\xef" + - "\xf9\xd2\xd4\xde\xcdWO\x9fo\xb6\xbf/\xf8\x8a\xc24" + - "\xca\xdd\x05\xe2\xbe\xa9@\xdc\xc2\x05\xf5\x0bw\xfd\xfbW" + - "\xdeJE\xdc\xe9\xc2/\x112\xde\xe6\xad\xb7M,\xb8" + - "\xf3\xddw\xd3\x11\xf7x\xc1\xf7\xccs\x052\xfcY\xe9" + - "!\xf9\xc5\xc7\xff\xe6=zHl\xb6\xe6\xf9\xc26\x94" + - "\x7fE\x0f\xad\xbdP\xf0u\x88#\x7f\xae\xb8x\xe7\xca" + - "N\x94/^Ir}p%\xc9u\xc3\x8en\xbe\xfd" + - "\xc6[\xde\x07\xa9\xc8f\xe4\xf1\xaa\xa5\x9d(\xdf\xb4\x94" + - "\x0e\xad[*\xa2\xfc+\xfa\xe8}cl\xdbk\x17{" + - "\x1f\xff\x9f\xe6\xcb}\x85\xce\xd1\x91w\xfc#\xbfX\xea" + - "\x9b\x7f\xed\x9a\xbf\xfa\xe0\xe8_\xf7^\x9cu\xfb\x8ab" + - "\x0f\xca\xeb\x8a$\xc7\x0d\xc5\xaf\xca\xbc\xe8_\xfe\xf5\xf5" + - "[\xd6-\x7f\xe5\xa3\xb4%6\x15?\"K\xa8E\xb2" + - "\xc4\xe8\x8d\xff\xfd\xd5\xab\xbf\xf1\xcf\x1f5\xb9\xc7g<" + - "P\\\x89\xf2\x11\xff\xc6\x07\x88\xf9\xc3\x8d\xdfy\xa3\x98" + - "+\xfef.A\xcf\x14'P\xfea\xd1\x8f\xc6\xa2/" + - "\xe8-\xbf|\xe4\x8e\xd2\xb7\x7f\xf31\xe9\xc5\x9aP\xea" + - "\xe2\x1fnC\xb9e\x19\xdd\x8c\xcb(U\x06O\xbd\xf9" + - "\x95\xf1\xa3?\xfa\xa4\xd9\x08\xbeCN/;\x88\xf2+" + - ">\xf7K\xcb\x08k\xf6}x\xac\xef\xc1\xed\xa7>M" + - "ku\xb2\xfd\x05\xdf\xbf\xed\xa4U\x9cJs\x05\xd2\xf9" + - "\xf6\x1e\x94/\xb4\xd3u\xef\xb4\x97`\x95\xe7\xb8\x86\xc1" + - "u\xab\x91\xa9\xfeI\xf4\xb1\xba\xba\xaa6\x8cFg\xaf" + - "i\x18\xbc\xeaT\xdcj\x95\xdb6@\x19Qic\x19" + - "\x80\x0c\x02H+\x1e\x01P\xaee\xa8\xdc(\xa0\x84X" + - " \x14\x95n\x98\x00P\xbe\xccP\xe9\x12\xd0\xb3\xb9\xb5" + - "\x87[\x83&VUG3\x8d\xcd*\xabs\\\x0c\x02" + - ".\x06\xf4\xaa\xba\xc6\x0d\xa7\xd7\x84\x9c1\xaa\x8da>" + - "\x09\x05@\xcc\x03\xce'\xd8\x86I\xcdv4cl\xd8" + - "\xa7\x97\xca\xa6\xaeU\xa7H\xba\xc5\x04\x1dR{'\xdd" + - "!]\xb1\x0d\x00\x05I\xea\x01(ic\x86iq\xaf" + - "\xa6\xd9UR\x0aX\xd5\xd9\xbfS\xd5U\xa3\xca\xe3\x87" + - "Zf?\xd4\xc7u\xdd\xbc\xd9\xb4\xf4\xda\x16K\x1b\xd3" + - "\x8c^\x93\x84\xf5-\x11\x1f\x13\xe70\x9c\xaf[\x85[" + - "{\xb4*_\xed\xda<8\xe7Z\xbe\x1d\xae\x1a\xe2\xb6" + - "\xab;6\x80\x92\x89\xad\x99\xed\x04P\xda\x18*\x05\x01" + - "K\x96\xcf\x80\xf9\x04\x13\x9al\xd2:\xfb\xcd\xc0\x16\x15" + - "\xdf\xe4\xab]\xc3\xe2c\x9a\xedp+ _U*\xab" + - "\x96Z\xb7\xd3\x0f\x92\xfb\xf2\x0c\x95e\x02zc\x96Z" + - "\xe5en\xa1f\xd66\xab\x86Ya\xbc\x8a- `" + - "\xcb\xfc\x8e\xd8\xa8j:\xaf\x05\xda\xad\xaev\xf8\xff\x95" + - "<\xcb,\xf6<\xff\x11u\x1b\x80\xb2\x83\xa1\xa2\x0b\x98" + - "\xc5O\xbd H\xb4\xbd\x00\xca8C\xc5\x110+\xfc" + - "\xce+\xf8^\xdb\xbd\x1c@\xd1\x19*\x93\x02f\xd9o" + - "\xbd\x02\x154\xc9\xa5\x80r\x18*wQ@\xb9\x0d\xb2" + - "\xa9\x0d\xcc\xb40\x9f$Yh\x1d^\x1b#K\x1bP" + - "\xe2U24\xe6\xa3Z\x100\x885s\x1c\xf3I\xa1" + - "\x0b\x8fY|\x0f\xb7l^\x86\x9ceNNa>\xa9" + - "\x09MV\xcf^\xae\xd5#G\xc7\xa7\xe6?_\x0d\xf2" + - "\xed\xaar\xc7,g\x91\x1d\x173T\x96\x0a\xe85\xe8" + - "[\xeep`\x96\x8d\xf9\xa4\x81h\x92v\x8ep\xee\xa5" + - "\xbfaV\x97\xc3[\xac0\xb1\x97\xc6\x8f\x1d\xa3\xc7\x1e" + - "f\xa8|7\x95\xd8\x8f[\x00\xcac\x0c\x95S\x02\xa2" + - "\x10x\xec\xa9\x13\x00\xca)\x86\xca\xdf\x0b(1!p" + - "\xd8s+\x01\x94g\x18*?\x11P\xca\xb0\x025K" + - "\xd2k\x14l?a\xa8\xfc\\@\xa9%S\xc0\x16\x00" + - "\xe9\xfcN\x00\xe5\x0d\x86\xca\xdb\x02zf\x90_\xa4\x94" + - "\x83Y\x100\xebc\x84\xe9\xd6Fu\x15:,^\xeb" + - "_\x1f\xd3\x0d\xb7^\xb6\xf8\x1e\x0dM\xd7\xeev\x1c^" + - "\x17\x1b\x8e\x8d\xad `+`\xceQ\xc7l\\\x02X" + - "f\x88\xf9\xa4\x04\x03\x121\xbe\x13-^\xdb\xca-[" + - "c\xa6\x11\x83\x92f8\xdcp\x06U\x10wr=\xa6" + - "\xce\x93uCa\xecP\xe4\x84i`&H\x81c\xca" + - "b\x96Y\xe6y\xa1\x117\x90m\xba\x18*\x83\x02\xb6" + - "\xe3\xa7D&;\xf6\x0f\x01(}\x0c\x95a\x01\xdb\x85" + - "\xdf\x11\x99,\xa9\x90\x1f\xca\x0c\x95\xed\x02\xe6\xc6\x1d\xa7" + - "\x81\xf9\xa4v\x87\xce\xbe\x83\xef\xb4\xcd\xea.\x0eHp" + - "\x11\x17\x92\xf0\xdb\xf1\x10\xbe\x80\xe95\xcc'\x9dsS" + - "\xa4\xb0\xcf\x82\xfe\x92\xb3\xc1\xb2L\xcbG\xd68<6" + - "\\\x9f(\x11EG\xff\xb6D\x03I\xe8\x0a\xd4Rv" + - "&\xf2wTU\xd7N\xc0\xdf\xe2\x8e5\xd5=\xea\x00" + - "\xe3V\x8c3\xf6\xb8\xe9\xea\xb5!\x0e\xa2cM!\x82" + - "\x808?\xfa\xac7\xfbR\x86\x0f\xc28%'\xc9\xb4" + - "\x9e\xa1RN\xe4\xdcD\xb4A\x86\xca-$gh\xfe" + - "\x112\xff0C\xa5!\xa0\xa7S\xfe\x1a}&0\xdb" + - "\x89\xc5\x0d\x88e\xd3\x0fN\x11\x04\x14\x01=\xb7a;" + - "\x16W\xeb\x80q\xb4\x11\xff\x92\xcb\x80\xe9&\xb8(\xab" + - "9?\xef\xe7\xd6!N\xc5M\x03i%\xc2\\\x1c\xe9" + - "I\x8c=w2\x8d\x9b\xb6c\xa8u\x0e\x00\x91b\xfb" + - "\xcd\x06\xe1$\xa1H\xdc\xd86\xc5\xc6\xe5W\xb7\xa0\xd2" + - "\xcc\xa8m'R\xa5\xa6\x1a\x9eF\xffx\xafi\x88\x97" + - "]\xfeC\x04\x0b\xd0uuX-\xa93\x89\xca\xce\x0a" + - "*%W1T\xaeK\x97\x9dUd\xa2k\x18*_" + - "\x16P\xe4\x16U\x90xB\x0a\x1e\xddo\x07\xad\x0e\xe6" + - "\x93\xe1\xf5\xd2\xe2t\xbb\xce87\x1c-\xe8rf\x85" + - "\xe1\xf2$]b\x17\xf6_\x9f\xf2k\xe4\xc2M;\x13" + - "\xbf\x8a\xbb\xf8T\xe4\xa5\x0e^W\xb5\x04\x8dB\xe7v" + - "\x83\xf8\xb5\x84g\xden),\x8bAQ,\x05\xde\"" + - "!\x0b\xb1\x90wN\x03(w1T\xeeO\x09y\xdf" + - "C\x00\xca\xfd\x0c\x95\x87SB\x1e%#\x1ef\xa8\x1c" + - "'\xccg\x01R\x1d#\x07\x1fg\xa8<) f\x02" + - "\xc8?I\x90\xff$C\xe5\xac\xe0\x03v_w\xafi" + - "`(\x84\x0d\x10\xc1\xb57\xceU\xcb\xd9\xc9Ut\xfa" + - "\x0d\x87[{T\xd4#H\xd8\xefhun\xbaN\x0c" + - "\x11uu\xd2o9\xb0\xd6\x17\x9c\x12U\xc7\xc6\x05 " + - "\xe0\x02\xcaH\x9b[\xbd\x16\xaf!yC\xd5\xcb*s" + - "\xc6?\x8f\x81f\x82xn\x0e\xf3P\xc3\xb2\x8f\xa1r" + - "/A\x09\xa6\xa6p\xe9\xee\x09\x10|$!\x9dw\xf7" + - "$-\x8c_\x10\xa9\xcc\xb9d\xc6I\x86\xca\xa1\xb0 " + - "\xb6\x02H\x07\xc8:\x87\x18*\x87\x85H\xb4>\x13J" + - "A\x866\xbb:\xec\x91\xf7\x13jj<\xd17\xec\x17" + - "44\x8da\xdfP\x98X\xaaj\xd6\x1b\x16\x85\xb2f" + - "\x1a\x8a\xab\xea\x1as\xa6\xe2\x83\xf3\xda\x82 )H\xe5" + - "-\x8d\x0e\xdfYd\x8c\xeb\"c\xc8\xdd8\x00P\xe9" + - "B\x86\x95AL\xc2E\xee\xc7\x1e\x80\xcaz\xa2\x971" + - "\x89\x18y\x13\x16\x01*}D\x1fF\x011\x88\x19Y" + - "\xc1\xa7\x01*\xc3D\xde\x81I\xab \xdf\xee_\xbf\x9d" + - "\xe8\xe3\x98t\x0b2\xc7\x95\x00\x95\x1dD\xdfG\xf4V" + - "\xc1\xb7\xa4<\x85\x13\x00\x95I\xa2\x1f\"\xba\xd8R\xa0" + - "AG>\x80\x16@\xe5.\xa2\xdfO\xf4\xb6\xa5\x05l" + - "\x03\x90\xef\xf3\xe9\xf7\x12\xfd[D_\xf0\x85\x02.\x00" + - "\x90\x8f\xe0A\x80\xcaa\xa2\x1f'\xfaB,\xe0B\x00" + - "\xf9\x18>\x02P9N\xf4'\x89\xbe\xa8\xb5\x80\x8b\x00" + - "\xe4\x93\xbe<\x8f\x11\xfd\x14\xc6\xb8\xd6_K\xc3+\x85" + - "\x95\x96\xb4\x17\xcc\xb4c\xd7\xf2p\x84\xc1\x00\xfb\xcbf" + - "\x8ef\x18\xcc%\x8b0@\xcc\x01z\x0d\xd3\xd47\xcf" + - "\x84\xedKu8aX@\xce4\xfakq\x9e\x05\xc1" + - "4hBGU\xd5\xfb\x1bI\xcfcw\xbb\x8e\xe96" + - "\xa0\xa3\xa6:\xbc\x16\x17^\xcb56Zf}\x18\xb9" + - "U\xd7\x0cU\x87\xf8\x9b\xf9b+\xe7\xbaZmV\xd2" + - "\x09\xcd\x81\xd6\xd1\xe8\x1cV\xc7\x9a\x06\xca\x95\x09j\xc7" + - " \xb4\xea\xfa\x04\xb4s\xe9\xe4\xe8\xd8\xa3\xea.\x9f\xf5" + - "\xd2\x1c]\xefHS\x85\x0a\xea\xc6\xacq\xb6'y=" + - "~\xdc\x0aG\xdc>!\xa9\x0d\x91\x15F\xc3\xd9\x07:" + - "\xe8\xee\x94?\xe2\x85I\xe8\x8f\xcf\xdb\x0d\x8cq'\xf8" + - "\xd4o\x8c\x9aTFE\xb5n\xff?O\x0fq;G" + - "\xa3\xc7%'\xccx\x05r\xe9:\xd77<\\N\xc6" + - "`\x16\x80d\x1a\x17\x86\xd2\xb8\x90\xc0\xc2D:\xfd\xa3" + - "\xeePV\xfc<,\x13};&\xf3\x83|+\x9e\x98" + - "\x91\xff\x99\xee\x00\x17\xb8\x7f}\x8d\xe8\x0d\x1f\x170\xc0" + - "\x85\xba\x7f\xbfN\xf4\xc94.\xb88=\x13\x17X\x84" + - "\x0b\x94\xcf\x87\x88~\xd8\xc7\x85L\x80\x0b\x0f\xe0\xb33" + - "\xf2\x7fAK\x80\x0b\xc7\xf0\x85\x19\xf9\xbf\xb05\xc0\x85" + - "\x93>\xff\x93D?\xeb\xe3BO\x80\x0bg|\x1cy" + - "\x86\xe8/\x12.\xb8\x96^q,\xcd\x00\x1cK\x82\xb5" + - "\xda\xf8\x1a\xe7\x8dn\xc8\xe9\xda\x1e\x1ecvMS\xf5" + - "\xf5\xae\xaaCG\xc5Q\xab\xbb\x92\x16X\xb7\xfbT\xa3" + - "f\xe3\xb8\xba\x8b\x13\xd2\x8b\xe9\x9a\xe8\xe8\xf6Vni" + - "\xa3\x80I\xd3\x1c\xf7\x08\xb9\xb2i6\xb7\x0e~\xef\xc5" + - "\xad\x00T\xe2\xef\xea\xead\x7fM\xe7\xbd\x18u\x0a\xcc" + - "H*\x8dF\xdf\x98\x86\x81A\xf9\x1e\xd6:f\xd6\xe5" + - "F\xd8\x86G\xf5}\xb8\xd4T\xb8\xf9d\x83W\x9d^" + - "\x13\x0dG3\\>\xeb\x82\xea\xb8k\xec\xe2\xb5\x0dh" + - "T\xcd\x9af\x8c\xc1\xac\xfe\x9f}\xd6\xf6!\xd5\xd0\xf8" + - "\xd9\x8c\xa9\xd5\xbd\xb4\xa2\x13\x04\x1fK\xa8" + - "\xab\xbd\x9f\xbbM\xecIZ\x99\x08#\x0fL'\x9dL" + - "\xbd\x1fZr\x89" + - "Q%=+\xf9\x15'\xe3GM\xf4\xeb\x08F\xbfS" + - "I\x12y?+z\xd1<\x85Q\xb9\"\xe7\xa5]v" + - "\x99C\xe5\x10\xef\xb0?O%\x88\xf6\xdb\x97\xde\x0d\x04" + - "\xef\xe4(\xd8\x02\x85\xe2{'R[*\xdd\x0c\xe7\xa1" + - "\xdc\xe6tK;\x8f\xad\x02\x81\xa3\x064G\x87\x9b\xc2" + - "oy\xaa\x93\x8e\xe3oy\xd2\x9b\xc7S\xca\xdd\x03a" + - "P>\x167\x9c\xd2\xa3\xd3\xc9\x06+\x0e\xbf\xa7\x06\x92" + - ")\xc5\x9f\x09C9E\xd7J`S7\xc7\x065\x83" + - "\xdb\xd4\x825M\xfa\x0dn\xd5U\x83\x1b\xe8\x10\x18\xb9" + - "\x16!\xeaL\xe4\xea_\x9f\xea\xdc\xe6S\xbf\x12\x06{" + - "\x10\xebayM\xcd\x91'R+\x96Hy\xe5\x85p" + - "u\xb1#\xa5\xfc\xed4Gng\xa8\x8c\x0b\xe8\xa9\xae" + - "c\x8e4j*:|\xa3\xc5w\xbb\\4\xaaS\xc9" + - "l\xc9c\xe2\xc7\xd8\x05T" + + "\x1cl\x82;\x98\x98\x8c\xed\xc6\xc5\xa6N\x83\x89\x996" + + "\x14\x98\xa4\xed\x14h\xd2\x12R\xe8\xc4)\x9d\x12\x8c\xa7" + + "\x13\xa6\x0c\x05\xd2a\xe8`\xb6s\xf6\xad+!\xd9\x99" + + "\xfe\xd1\x7f\xec\x9ds\xcf\xf78\xaf\xdfy|\xba\xf5\xe5" + + "\x9a\x16nU\xd5\xcey\x00\xf2\x99\xaa9.[\xf1\xb3" + + "]'\x96\xfd\xed~\x90\x1b\x10\xdd\xaf?\xdfU\xff\x89" + + "\xbd\xff_\xa1\x8a\x17\x00\x9a~K\xd8\x85\xd2jA\x00" + + "\x90V\x09\xff\x01\xe8V\xfd\xf6\x1bo\x95\xdf\x12\x0e\x80" + + "\xd8\x90d\xe6\x88y~u\x17J\xcb\xab\x89yY\xf5" + + "N@\xf7\x0fJ\xaf\x9e\xfa\xdd\xa3?&f.f\x06" + + "l:T\xbd\x0b\xa5\x93\x1e\xe7\x13\xd5\x1b\x01\xdd\x8f\x1e" + + "\xbd\xfe\xcfO\xfe\xd3\xcb\x07A\xfc\x12Bp\xf6\xb3\xd5" + + "\xbf@@\xe9\x95\xea\xef\x01\xba\xff|\xeb\xeew\xb6~" + + "t\xe4\x81\xc9\xe7\xa6\x88O\xad\x99@i_\x8d\x00\xbc" + + "\xfb\xc4\xbd\xf5\xff\x80'>>\x02\xe2M\xb4\x0d\xd2\xcf" + + "[jj9@\xc9\xa9\xc9\x01\xba\xaf\xde\xfc\xfcs\x87" + + "\xbf\x7f\xff\xb7@\xfe\x12\"\xf8\xeb\x8f\xd6\xfc\x0f\x9ds" + + "\xd6c\xf8\xe0;_N}\xf7\xd5/~\xdbcpO" + + "\xbfv\xd73\x87\xbf\xbf\xf8]\xe8\xe7\x04L\x014\xbd" + + "Vc\x12\xef\xbf\xd5\x90.\x1e\xfd\xf9\x0b\x1bJG\x1e" + + "?\xe5_\xda\xdb\xeb\x85Z\x8e\x83\x94\xbb\xfa\x17\x977" + + "\xae\x7ff\xe8\xc9@\x1c\xef\x1e\xe7k\x9f\xa1\xa5?\xaa" + + "\xa5c^\xde\x99y\xa8\xf5\xf7\x1e~\xb2R\xe9U\xc4" + + "y\xb9v\x02\xa5+\xb5\xf4\xf9I\xed]\x08\xe8N\xdc" + + "\xf1\xc2\xa6\x8f\xfe\xc8z\x0a\xe4[0\xe5\xfe\xdd\x83\x97" + + "v,?;\xf4\x92w+\x1e\xa0i\xfb\xdc\x9f\xd1\xd6" + + "\x07\xe7\x92\xa6\xea\xfez\xc5\x86\x87\xdf\xe9>O['" + + "\xb4\xee_bY]3J\xab\xeb<{\xd6\x11\xf7O" + + "o\xde\xf4\xe2\x8bO\x0f\x9f\xaf\xbc\x88g\xd0\xd7\xea\xba" + + "P\xba\xecq\xff\xd2\xe3\x9e\xdf\x89o\xfepU\xea/" + + "\x93f::\xef]O}\xf3\x88\xe1\xdeO\x9f\xfd\x9b" + + "\xb5\xef\xbf\xfe\x83\xa4\x01:\xd3\x1c\x19`K\x9a\x04\x1f" + + "\x98\xc0\xd2\x9b\xcd-/\x82|\x13\xa2;zt\xb7\xdd" + + "\xf1\xd8!\x17\xfaQ@\x0e\xa0\xe9`z\x17mv$" + + "M\xee\xd3\xf8^[\x9d\xfe\xfe\xfe\x1fV\xf8\x9aw\xea" + + "{\xe9.\x940CW\xbb\x92\xfe\x1e\xe0\xc7O\xdd\x7f" + + "\xb8\xf3\xd2\x9a\x97\xe4\x06LU\x0a},\xb3\x0b\xa5\xef" + + "\x12o\xd3\xd9L\x96\xf4\x19i\xb0\x82\xdd\x93\xfa\xa28" + + "\x8a\xd2{\"}\xfeJ\xf4\xd8\xbb\xee\xfd\xe6#U\x97" + + "\xbf\xf9R\xa5J).\x9a\xfe\xfb\x8b&J5\x12}" + + "VIOr\x80n\xc3\xd3\xbf\xff\x17m\xc5\x8b?\x9e" + + "&F\xa4\x0f\xe6\x7f(]\x99O_\x9f\xcc'\x19\xef" + + "\xff\xf2\xf8\xae\x0d\xcb&\xde\xa8\xd4\xbfw\xf1\xfe\x05\x13" + + "(\x95\x16\x10\xb7\xba\x80\xb8\xb9\xcb\xca\xc2\xbd\xff\xf2\xd5" + + "7\x13\x1ewq\xc1\xdb\x08)w\xc3\xa6{Gk\xee" + + "\xbbt)\xe9q\xff\xb8\xc0\xb3\xcc/\x17\x90\xe2/\x88" + + "\x8fH\xcf\x9f\xfc\xb3w\xe8 \xa1R\x9bx\xdd\x00J" + + "\xf3\xaf\xa3O\xf1:O\x86\xc8\xf3\xa7\xf3\x8b\xba\x86f" + + "\x94\x1a\x1b\xe8^\x0b\x1b\xe8^\xab\xb7\xb6\xb2\xcd\xb7\xdf" + + "\xfd.\x88\x0d\xfc\xa4@\xbf\x878U\xe2lb\x0dw" + + "\xa2\xd4z\x83\x00\xe0~cx\xe0\x95\x0f\xdaO\xfeW" + + "\xe5\xe6\x9e@\xcbohF\xe9\x0e\xe2kZ}\x83\xa7" + + "\xfe\xa6U\x7f\xfc\xde\xd1?m\xff`\xca\xee\xfb\x16\xb5" + + "\xa1td\x11\xdd\xe3\xd0\xa2;\xa5\x17\x16y\x9b\x7f}" + + "\xcd\xc6;\x96\xfc\xe8\xc3\xa4&N/\xfa\x904\xf1\x83" + + "E\xa4\x89\xa1\xdb\xff\xf3\xcee\xdf\xf8\xfb\x0f+\xcc\xe3" + + "1^\\\xb4\x02\xa5_y;^&\xe6\xf7\xd7}\xfb" + + "\xf5\x86t\xc3\xaf\xa7\xbbhM\xe3(J\x8d\x8d\xf4\xb9" + + "\xb0\xd1\xbb\xe8\xddo?\xbe3\xf7\xad_\x7fLr\xf1" + + "\x15(\xd5y\xe3\x00J[n\xa4\x9d\xef\xb9\x91B\xa5" + + "\xfb\xdc\xc5\xaf\x8e\x1c}\xf9\x93iq\xb7*\xbb\x1f\xa5" + + "\x85Y\xe2\x9e\x9f%\xac\xf9\x13\xe1\xf8\xa5\xbd\xff\xfe\x87" + + "\x9f&\xa5\xba\x92}\x9b\xa4\x12\x17\x93T\xbb\xdf?\xd6" + + "\xf1\xf0\xe6s\x9f%\x19V-~\x8e\x18Z=\x86(" + + "\xd6\xa6\xf34eq\x1bJ\xdb\x17\xd3y\xa5\xc59\xb8" + + "\xc5\xb5\x1d]g\x9aYN\x15~'\xfc,\xac,(" + + "e\xbd\xdc\xdc\xea\xd8#L\xb7\xd5\x82b\xb3\x1e\x96\xb3" + + "\xca\x86n\xb1<\xa2\x9c\xe1S\x00)\x04\x10\x95Q\x00" + + "y+\x8f\xb2\xc6\xa1\x88XOX+\xaaD\x1c\xe1Q" + + "\xb69\x149\xae\x9e\x02^\xdc\xbe\x04@\xd6x\x94\xc7" + + "8D\xbe\x9e\xe0Lt\x1e\x01\x90\xc7x\x94\x0fp\xe8" + + "\x96\x99YRt\xa6C\xda^k\x9a8\x178\x9c\x0b" + + "\xe8\x9a\xcc6\xc7\x95A\x0d\xd2,A\x16Fw\xdaX" + + "\x07\x1c\xd6\x01\xba#\x86cZ\xfd\xba\x8d\xaa\xd6\xc3\x86" + + "Lf\xe1\x08\xce\x01\x0e\xe7\x00\xce$^\xbb\xa1\xeb\xac" + + "`\xf7:\x85\x02\xb3,\x00\x92\xac:\x92l\xf9\xe3\x00" + + "\xf2\xcd<\xca\xb7'$[M\x92}\x85G\xb9\x85C" + + "\xd7b\xe6\x0efv\x1bXPl\xd5\xd07(|\x89" + + "E\xd7.h*\xd3\xedv\x03\xd2\xfa\x90:\x8c\x998" + + "\x14\x0013\xf3\xc5\xd6\x8e\xa9\x96\xad\xea\xc3}\x1e=" + + "\x9774\xb50N\xb7\x9b\xebi\xb2\xb1\x99\xf6\x10\xe7" + + "\x0f\x00 '\x8am\x009uX7L\xe6\x16U\xab" + + "@B\x01_\xb0\xf7\x0c*\x9a\xa2\x17Xt\xd0\x9c\xa9" + + "\x07\xf9\x07\xf4zr\xacT\x12\xd6^\x9aWL\x85/" + + "Y\xf2\xdcH\x1fk\x07\x00\xe45<\xca\xf9\x84>\xd6" + + "w\x01\xc8\xdd<\xcaw',\xdd\xdf\x06 \xe7y\x94" + + "7s\xe8\x1a\xa6:\xac\xea\xed\x0cx3i0\xcb\xd6" + + "\x95\x12\x03\x80Pa{\x8c2)\xd1\xc2L\x0c\xc2\x15" + + "\x9a\xaa\x9a*@\x07\xd34\xe3.\xc3\xd4\x8a\x1b\xfds" + + "\x0c\xd2\xb6g\xcah\x990\x8d\xe5=\xe3\x90\xdcj\x81" + + "\xadt,\xe6\xafsL\xcf\x90K{\x98\xe5h\xb6\x05" + + " \xa7\"\xf1\xeb\x9a\x01\xe4j\x1e\xe5z\x0es\xa6\xc7" + + "\x80\x99\x18\xd4+\xae:\x9b\xae\x1d\xddd\xc3\xaae3" + + "\xd3'/\xcd\x91\xc2KV\xf2@\xf2\xbf\x0c\x8f\xf2\"" + + "\x0e\xddaS)\xb0<3Q5\x8a\x1b\x14\xdd\xe8\xe5" + + "Y\x01\xab\x80\xc3\xaa\x99=i\x9d\xa2j\xac\xe8K\xb7" + + "\xb2\x90\xf5\xfe\xa7\xe8\x9d\xeb\xba~\xf8\x0e\xc4\xe1[\x87" + + "\x9f\xb9A\xfc\xee\x8a\xe3\xb7\x8e\xbb\xe2N\x0d\xe0:\xfe" + + "S7\x08a\x8a\x08\x9bGy/E\x84S&\x9dZ" + + "\xc0\x1b&fb\x94\x0c\xb4\xc3\x8a\xc3\xa4i\x1dr\xac" + + "@\x8a\xc6L\x98\xcc}\x06\xa1h\x8c`&\xaeT\x82" + + "e&\xdb\xc1L\x8b\xe5!m\x1ac\xe3\x98\x89\x93z" + + "\x85\xd6\xeb\xaeU\xeb\xa1\xa1\xa3U3\xaf/\xf8\x80\xb1" + + "4\x9f\x9db,\xd2\xe3\\\x1e\xe5\xeb\x09\xc8\xe8Wf" + + "\x93\xcb\x93;G\x15\xe0\xec\xee\xdcN\xff\x06\xb0\x94\x0f" + + "v1\x03d\xba>:\xec\x18\x1d\xf6\x18\x8f\xf2w\x12" + + "\x91x\xd2\x04\x90O\xf0(\x9f\xe3\x10\x83@<{\x0a" + + "@>\xc7\xa3\xfcW\x1c\x8a<\xe7\x1b\xec\xd9\x15\x00\xf2" + + "\xd3<\xca?\xe1PL\xf1\xf5T\xed\x8a\xaf\x90\xb3\xfd" + + "\x84G\xf9\xe7\x1c\x8aU\xa9z\xac\x02\x10\xdf\x18\x04\x90" + + "_\xe7Q~\xeb\xf3\xe2\xb8\xa0\x19NqHS k" + + "\xb2b\xe7\x9a\x88\xae;\xa5\xbc\xc9v\xa8h8V\xab" + + "m\xb3\x92P\xb6\xad\x10\x92\xd3\xb62l\xe1<\xc0<" + + "\x8f\x98\x89k(@\"F{\xa2\xc9\x8a\x9b\x98i\xa9" + + "\xbc\xa1G\xa8\xaa\xea6\xd3\xedn\x05\x84A\xa6E\xd4" + + "\x19\xa2\xae'\xf0\x1d\xf2\x9c \x0c\x8c\x18)p\x98\x00" + + "n\x91\xeb\x06J\\K\xbai\xe1Q\xee\xe6\xb0\x11?" + + "#2\xe9\xb1\xb3\x07@\xee\xe0Q\xee\xe3\xb0\x91\xbbB" + + "d\xd2\xa4<\x10\xe3\\z\xc4\xb6\xcb\x98\x89\x8b\xaf\xc0" + + "\xd8;\xd9\xa0e\x14\xb61@\x82\x8b\xa8\x12\x08~\x1d" + + "\x09\xe0\x0bx\xad\x88\x99\xb8\xf5\xa9\xf0\x14\xfe\xf3rW" + + "\x8e2\xa5az\xa9!\x06\xea\xdbb!B\xef\xe8\x1c" + + "\x88%\x10\xb9\x16_,y0\xbe\x7f\xb6\xa08\x16\x9b" + + "\x9ct[\x87l\xe0\x99\x19\xe1\x8c5b8Z\xb1\x87" + + "\x81`\x9b\xe3\x88\xc0!\xce\x8c>k\x8c\x8e\x84\xe2}" + + "7\x9e>\xa1D\xf9d \x99O\x02\xf5\xf7\x93\xfa\xfb" + + "x\x94\xcb\x1c\xba\x1a\xc5\xaf\xdea\x00o\xd9\xd1u}" + + "b\xde\xf0\x9cS\x00\x0e\x05@\xd7)[\xb6\xc9\x94\x12" + + "`\xe4m\xc4?\xef\x1a`\xba\x02.\xf2J\xda\x8b\xfb" + + "\xffOI\xf1\xda\xb3\x9b\x9fi&\xe5\xb6S\x89TS" + + "\x08V\xa3\xb7\xbc\xdd\xd0\x85k\xae_\x02\x04\xf3\xd1u" + + "e\x90-\xa9\xb4\x0a\xd3\xcerJ%Ky\x94oM" + + "\xa6\x9d[HE7\xf1(\x7f\x85C\x81\x99\x94A\xa2" + + "\x16\xd7?t\x8f\xe5\xd7j\x98\x89\xc7\x13\xb3_'Q" + + "\xc6\xaa\x86>\xc5\x0d\x97\xc4\xe1\x12\x99\xb0\xf3\xb6\x84]" + + "C\x13\xae\x1f\x8c\xed*lc\xe3\xa1\x95\xb2\xac\xa4\xa8" + + "1\x1a\x05\xc6m\x05\xe1k1\xcf\x8c\xe5^\x90\x16\xfd" + + "\xa4\x98\xf3\xadE\x97\xac\x8f.y\xdf\x04\x80\xbc\x97G" + + "\xf9\xa1\xc4%\x1f\xa4\xea\xf9!\x1e\xe5\xc7\x12\x97\xe4\x9f&\xc8?\xc3\xa3|\x81\xf3\x00" + + "\xbb\xa3\xb5\xdd\xd01\xb8\x84\x05\x10U\xd0#L1\xed" + + "A\xa6\xa0\xdd\xa9\xdb\xcc\xdc\xa1\xa0\x16B\xc2\x1e[-" + + "1\xc3\xb1#\x88()c^\xc9\x81\xc5\x0e\x7f\x95\xa0" + + "\xd8\x16\xd6\x00\x875\x14\x91\x163\xdbMVD\xb2\x86" + + "\xa2\xe5\x15\xde\x1e\xb9\x1a\x05M\x06\xf1\xf44\xea\xa1\x82" + + "e7\x8f\xf2\x03\x04%\x98\x18\xa3\x88\x07G\x81\xf3\x90" + + "\x84d\xde\xde\x16\x970^B\xac\xaahB\xbc\x848" + + "\x07@\xdcG\xda9\xc0\xa3|\x98\x0b\xaf\xd6a@\xce" + + "\x8f\xd0JS\x07E\xfe\x1eBM\x95\xc5\xf2\x06\xf5\x82" + + "\x8a\x86\xde\xe7)\x0acM\x15\x8cR\xd9$WV\x0d" + + "]v\x14M\xe5\xed\xf1h\xe1\x8c\xba H\xf2Cy" + + "c9\xeb\x19\x8b\x94qk\xa8\x0c\xa9\x15\xbb\x00z[" + + "\x90\xc7\xden\x8c\xddE\xea\xc46\x80\xde5D\xcfc" + + "\xec1\xd2zl\x00\xe8\xed z\x1fF\xbd\x99$\xe3" + + "S\x00\xbd}D\xde\x8aq\xa9 m\xf1\xb6\xdfL\xf4" + + "\x11\x8c\xab\x05\x89\xe1\x0a\x80\xde\xadD\xdfM\xf49\x9c" + + "\xa7Ii\x1cG\x01z\xc7\x88~\x80\xe8BU=5" + + "\xa2\xd2>4\x01z\xf7\x12\xfd!\xa2W__\x8f\xd5" + + "\x00\xd2\x83\x1e\xfd\x01\xa2?J\xf4\x9a\x85\xf5X\x03 " + + "\x1d\xc1\xfd\x00\xbd\x87\x89~\x9c\xe8\xb5X\x8f\xb5\x00\xd2" + + "1|\x1c\xa0\xf78\xd1\xcf\x10\xfd\x0bs\xea\xf1\x0b\x00" + + "\xd2i\xef>'\x88~\x0e#\\\xeb,&\xe1\x95\xdc" + + "J\x8d\xcb\x0b\xde\xb0\"\xd3\xb2\xa0\x07C\x1f\xfb\xf3F" + + "\x9a\x9a0L\xc7\xa3N@L\x03\xbae\xc3\xd06L" + + "\x86\xed\xd9*\x9c\xc0- m\xe8\x9d\xc5(\xce|g" + + "\xea6 [P\xb4\xcer\\\xf3X\xad\x8em8e" + + "\xc8\x16\x15\x9b\x15\xa3\xc4k:\xfa:\xd3(\xf5!3" + + "K\xaa\xaeh\x10\xfd2\x93o\xa5\x1dG-N\x09:" + + "\xae\xd2\xd1\xb2\xe5\xe6>e\xb8\xa2#^\x11\xa3v\x04" + + "B\xb7\xdc\x16\x83v:\x19\x1c\xd9\x1d\x8a\xe6\xb0)'" + + "MS\xf5\xf6Wd(?oL\xe9\xc7\xdb\xe2\xd3\xa3" + + "\xc3\xcd\xa0G\xef\xe0\xe2\xdc\x10ja(\xe8} K" + + "{'\xec\x11M\xbc\x02{\\m50\xccl\xff\xab" + + "S\x1f2(\x8d\x0aJ\xc9\xfa\x0dW\xf70+M\xad" + + "\xc7\xac\x1df4\xc3\x9a=\xcfu\xf4\xf5\xe5\xe36\x98" + + "\xf7A2\x89\x0b=I\\\x88aa4\x19\xfeau" + + "(\xc9^\x1c\xe6\x89\xbe\x19\xe3\xfeA\xba\x07OM\x8a" + + "\xffT\xab\x8f\x0b\xcc\xdb\xbeH\xf4\xb2\x87\x0b\xe8\xe3B" + + "\xc9\xdb_#\xfaX\x12\x17\x1c\x9c\x98\x8c\x0b|\x88\x0b" + + "\x14\xcf\x07\x88~\xd8\xc3\x85\x94\x8f\x0b\x87\xf0\x99I\xf1" + + "_S\xe5\xe3\xc21|nR\xfc\xd7\xce\xf1q\xe1\xb4" + + "\xc7\x7f\x86\xe8\x17<\\h\xf3q\xe1\xbc\x87#O\x13" + + "\xfdy\xc2\x05\xc7\xd4zmS\xd5\x01\x87cg-\x94" + + "\xbf\xc6X\xb9\x15\xd2\x9a\xba\x83E\x98]T\x15m\x8d" + + "\xa3h\x90\xed\xb5\x95\xc2\xb6\xb8\x04\xd6\xac\x0eE/Z" + + "8\xa2lc\x84\xf4B2'\xda\x9a\xb5\x89\x99\xea\x10" + + "`\\4G5B:o\x18\x95\xa5\x83W{1\xd3" + + "\x07\x95\xe8\xb7\x922\xd6Y\xd4X;\x86\x95\x02\xaf\xc7" + + "\x99F\xa5_\x0c]G?}\xf7\xa9\xd9\xc9y\xb9\x1c" + + "\x94\xe1a~\xef\xcbU$n6Vf\x05\xbb\xdd@" + + "\xddVu\x87M\xd9\xa00\xe2\xe8\xdbXq-\xea\x05" + + "\xa3\xa8\xea\xc30\xa5\xfe\xe7?o\xfa\x90(h\xbch" + + "\xc6\xc4\xdb\x8b\xb8\xbc\x198\x0fK(=\x8b\xcdq\x17" + + "\x9d+x\xabr&S\xacD\x038\xc3i\xc1\xb4\xcc" + + "\x0f2\xbf>\xa8\x02\x88^20\x1c\x17\x8bGv\x01" + + "'>(`\x9a\x97\x18\x1fa\xd8\x8a\x0a\x93\xb2Ar" + + "\x984o\x96\xbe&\xd9Xy\xe9)\xe5\xb9X\xf8\x16" + + "\x86\xe1\xab\xa4(\x92\xab\xd4\x09n\xd8|a\x98\xdb\xc8" + + "\x9aI\x93]c\x07\xda\xc3\xb2\xd6\xd5\xa4\x8d\xf05c" + + "\xf6A\x82\x7fN\x9a\xbc\xcf\x17(\xdaw41\xd2\xd2" + + "\x8c\xa0yJoH\xd6\xbf3\xe8\xca\xbfpX\xad\xa6" + + "iq\x85\xfb-I\x94\xdd\x91\xff-\x89\x0b\xf9\xa8\xa5" + + "9\xd8\x158\xe5\x89\xf8\xe5\xe0\x89\x89x\xdc\x15\xb9\xdf" + + "\xd9\xae\xb8\xa5\xf1\x1a\xc8\xf0\xb1\xc01c\x8c\xd5\x8c\xe1" + + "nUg\x16\xd5k\x15c\x81\xf0\x05\x02mB.\xc7" + + "$\xf8\x9d\x0cs\x9dk\x12e\xdeL\xe2\xf7\x06\xce\xee" + + "\xfbz\x90\x8b\x13M\xe7\xa9\xc4<&\x14^~.\x98" + + "slM\x08\xbf\x85\x9a\xce\xcd<\xca#\x9c\x17\xfcF" + + "\x7f\xb9\xa8\xa0\xcd\xd6\x99l\xbb\xc3\x04\xbd0\x1e7_" + + "\xd4~\x14\xac~,S\xa1\xb8\xced\xb9\xed\x0eK2" + + "\x84Sd\x10T\xa38e|\x11\x1b|\xfaT\xfc\x7f\x93=\x7f\xa3G\x10*" + + "D\x85\xab)\xd2\xa2\xbf\xac\x98}\xc4;\xed@\xb9'" + + "\xc7\xae*\xac\xe3\xa7\xbdk\x1c\xc2@\x14\xdb\x98x\x99" + + "\xa7C\xb8`\xf3\xff\x0d\x00\x00\xff\xff\xdfs7\x9d" func init() { schemas.Register(schema_db8274f9144abc7e, + 0x82c325a07ad22a65, 0x8407e070e0d52605, 0x84cb9536a2cf6d3c, + 0x85c8cea1ab1894f3, 0x8891f360e47c30d3, 0x91f7a001ca145b9d, 0x9b87b390babc2ccf, @@ -3939,6 +4312,7 @@ func init() { 0xf41a0f001ad49e46, 0xf7f49b3f779ae258, 0xf9c895683ed9ac4c, + 0xfc5edf80e39c0796, 0xfeac5c8f4899ef7c, 0xff8d9848747c956a) } From 0676923d244ecc6db70c95cb0f9a96d44b74db7b Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Mon, 18 Nov 2019 13:56:04 -0600 Subject: [PATCH 04/21] TUN-2551: TunnelRPC definitions for ReconnectTunnel flow --- tunnelrpc/pogs/reconnect_tunnel.go | 70 +++ tunnelrpc/pogs/tunnelrpc.go | 13 +- tunnelrpc/pogs/tunnelrpc_test.go | 30 ++ tunnelrpc/tunnelrpc.capnp | 5 + tunnelrpc/tunnelrpc.capnp.go | 741 ++++++++++++++++++++--------- 5 files changed, 620 insertions(+), 239 deletions(-) create mode 100644 tunnelrpc/pogs/reconnect_tunnel.go diff --git a/tunnelrpc/pogs/reconnect_tunnel.go b/tunnelrpc/pogs/reconnect_tunnel.go new file mode 100644 index 00000000..aef2f66f --- /dev/null +++ b/tunnelrpc/pogs/reconnect_tunnel.go @@ -0,0 +1,70 @@ +package pogs + +import ( + "context" + + "github.com/cloudflare/cloudflared/tunnelrpc" + "zombiezen.com/go/capnproto2/server" +) + +func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnectTunnel) error { + jwt, err := p.Params.Jwt() + if err != nil { + return err + } + hostname, err := p.Params.Hostname() + if err != nil { + return err + } + options, err := p.Params.Options() + if err != nil { + return err + } + pogsOptions, err := UnmarshalRegistrationOptions(options) + if err != nil { + return err + } + server.Ack(p.Options) + registration, err := i.impl.ReconnectTunnel(p.Ctx, jwt, hostname, pogsOptions) + if err != nil { + return err + } + result, err := p.Results.NewResult() + if err != nil { + return err + } + return MarshalTunnelRegistration(result, registration) +} + +func (c TunnelServer_PogsClient) ReconnectTunnel( + ctx context.Context, + jwt []byte, + hostname string, + options *RegistrationOptions, +) (*TunnelRegistration, error) { + client := tunnelrpc.TunnelServer{Client: c.Client} + promise := client.ReconnectTunnel(ctx, func(p tunnelrpc.TunnelServer_reconnectTunnel_Params) error { + err := p.SetJwt(jwt) + if err != nil { + return err + } + err = p.SetHostname(hostname) + if err != nil { + return err + } + registrationOptions, err := p.NewOptions() + if err != nil { + return err + } + err = MarshalRegistrationOptions(registrationOptions, options) + if err != nil { + return err + } + return nil + }) + retval, err := promise.Result().Struct() + if err != nil { + return nil, err + } + return UnmarshalTunnelRegistration(retval) +} diff --git a/tunnelrpc/pogs/tunnelrpc.go b/tunnelrpc/pogs/tunnelrpc.go index 0e99cffe..b675f0f2 100644 --- a/tunnelrpc/pogs/tunnelrpc.go +++ b/tunnelrpc/pogs/tunnelrpc.go @@ -33,11 +33,12 @@ func UnmarshalAuthentication(s tunnelrpc.Authentication) (*Authentication, error } type TunnelRegistration struct { - Err string - Url string - LogLines []string - PermanentFailure bool - TunnelID string `capnp:"tunnelID"` + Err string + Url string + LogLines []string + PermanentFailure bool + TunnelID string `capnp:"tunnelID"` + RetryAfterSeconds uint16 } func MarshalTunnelRegistration(s tunnelrpc.TunnelRegistration, p *TunnelRegistration) error { @@ -63,6 +64,7 @@ type RegistrationOptions struct { RunFromTerminal bool `capnp:"runFromTerminal"` CompressionQuality uint64 `capnp:"compressionQuality"` UUID string `capnp:"uuid"` + NumPreviousAttempts uint8 } func MarshalRegistrationOptions(s tunnelrpc.RegistrationOptions, p *RegistrationOptions) error { @@ -328,6 +330,7 @@ type TunnelServer interface { UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error) Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error) + ReconnectTunnel(ctx context.Context, jwt []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) } func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer { diff --git a/tunnelrpc/pogs/tunnelrpc_test.go b/tunnelrpc/pogs/tunnelrpc_test.go index 3a5ff7a4..98f061e0 100644 --- a/tunnelrpc/pogs/tunnelrpc_test.go +++ b/tunnelrpc/pogs/tunnelrpc_test.go @@ -11,6 +11,36 @@ import ( capnp "zombiezen.com/go/capnproto2" ) +func TestTunnelRegistration(t *testing.T) { + testCases := []*TunnelRegistration{ + &TunnelRegistration{ + Err: "it broke", + Url: "asdf.cftunnel.com", + LogLines: []string{"it", "was", "broken"}, + PermanentFailure: true, + TunnelID: "asdfghjkl;", + RetryAfterSeconds: 19, + }, + } + for i, testCase := range testCases { + _, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) + capnpEntity, err := tunnelrpc.NewTunnelRegistration(seg) + if !assert.NoError(t, err) { + t.Fatal("Couldn't initialize a new message") + } + err = MarshalTunnelRegistration(capnpEntity, testCase) + if !assert.NoError(t, err, "testCase #%v failed to marshal", i) { + continue + } + result, err := UnmarshalTunnelRegistration(capnpEntity) + if !assert.NoError(t, err, "testCase #%v failed to unmarshal", i) { + continue + } + assert.Equal(t, testCase, result, "testCase index %v didn't preserve struct through marshalling and unmarshalling", i) + } + +} + func TestConnectResult(t *testing.T) { testCases := []ConnectResult{ &ConnectError{ diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index 8cffa64d..a8110a9b 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -19,6 +19,8 @@ struct TunnelRegistration { permanentFailure @3 :Bool; # Displayed to user tunnelID @4 :Text; + # How long should this connection wait to retry in seconds, if the error wasn't permanent + retryAfterSeconds @5 :UInt16; } struct RegistrationOptions { @@ -44,6 +46,8 @@ struct RegistrationOptions { # cross stream compression setting, 0 - off, 3 - high compressionQuality @10 :UInt64; uuid @11 :Text; + # number of previous attempts to send RegisterTunnel/ReconnectTunnel + numPreviousAttempts @12 :UInt8; } struct CapnpConnectParameters { @@ -287,6 +291,7 @@ interface TunnelServer { unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> (); connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult); authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse); + reconnectTunnel @5 (jwt :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); } interface ClientService { diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index 75f03a41..b34167dc 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -234,6 +234,14 @@ func (s TunnelRegistration) SetTunnelID(v string) error { return s.Struct.SetText(3, v) } +func (s TunnelRegistration) RetryAfterSeconds() uint16 { + return s.Struct.Uint16(2) +} + +func (s TunnelRegistration) SetRetryAfterSeconds(v uint16) { + s.Struct.SetUint16(2, v) +} + // TunnelRegistration_List is a list of TunnelRegistration. type TunnelRegistration_List struct{ capnp.List } @@ -468,6 +476,14 @@ func (s RegistrationOptions) SetUuid(v string) error { return s.Struct.SetText(6, v) } +func (s RegistrationOptions) NumPreviousAttempts() uint8 { + return s.Struct.Uint8(4) +} + +func (s RegistrationOptions) SetNumPreviousAttempts(v uint8) { + s.Struct.SetUint8(4, v) +} + // RegistrationOptions_List is a list of RegistrationOptions. type RegistrationOptions_List struct{ capnp.List } @@ -2845,6 +2861,26 @@ func (c TunnelServer) Authenticate(ctx context.Context, params func(TunnelServer } return TunnelServer_authenticate_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} } +func (c TunnelServer) ReconnectTunnel(ctx context.Context, params func(TunnelServer_reconnectTunnel_Params) error, opts ...capnp.CallOption) TunnelServer_reconnectTunnel_Results_Promise { + if c.Client == nil { + return TunnelServer_reconnectTunnel_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))} + } + call := &capnp.Call{ + Ctx: ctx, + Method: capnp.Method{ + InterfaceID: 0xea58385c65416035, + MethodID: 5, + InterfaceName: "tunnelrpc/tunnelrpc.capnp:TunnelServer", + MethodName: "reconnectTunnel", + }, + Options: capnp.NewCallOptions(opts), + } + if params != nil { + call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 3} + call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_reconnectTunnel_Params{Struct: s}) } + } + return TunnelServer_reconnectTunnel_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} +} type TunnelServer_Server interface { RegisterTunnel(TunnelServer_registerTunnel) error @@ -2856,6 +2892,8 @@ type TunnelServer_Server interface { Connect(TunnelServer_connect) error Authenticate(TunnelServer_authenticate) error + + ReconnectTunnel(TunnelServer_reconnectTunnel) error } func TunnelServer_ServerToClient(s TunnelServer_Server) TunnelServer { @@ -2865,7 +2903,7 @@ func TunnelServer_ServerToClient(s TunnelServer_Server) TunnelServer { func TunnelServer_Methods(methods []server.Method, s TunnelServer_Server) []server.Method { if cap(methods) == 0 { - methods = make([]server.Method, 0, 5) + methods = make([]server.Method, 0, 6) } methods = append(methods, server.Method{ @@ -2938,6 +2976,20 @@ func TunnelServer_Methods(methods []server.Method, s TunnelServer_Server) []serv ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, }) + methods = append(methods, server.Method{ + Method: capnp.Method{ + InterfaceID: 0xea58385c65416035, + MethodID: 5, + InterfaceName: "tunnelrpc/tunnelrpc.capnp:TunnelServer", + MethodName: "reconnectTunnel", + }, + Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error { + call := TunnelServer_reconnectTunnel{c, opts, TunnelServer_reconnectTunnel_Params{Struct: p}, TunnelServer_reconnectTunnel_Results{Struct: r}} + return s.ReconnectTunnel(call) + }, + ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1}, + }) + return methods } @@ -2981,6 +3033,14 @@ type TunnelServer_authenticate struct { Results TunnelServer_authenticate_Results } +// TunnelServer_reconnectTunnel holds the arguments for a server call to TunnelServer.reconnectTunnel. +type TunnelServer_reconnectTunnel struct { + Ctx context.Context + Options capnp.CallOptions + Params TunnelServer_reconnectTunnel_Params + Results TunnelServer_reconnectTunnel_Results +} + type TunnelServer_registerTunnel_Params struct{ capnp.Struct } // TunnelServer_registerTunnel_Params_TypeID is the unique identifier for the type TunnelServer_registerTunnel_Params. @@ -3808,6 +3868,207 @@ func (p TunnelServer_authenticate_Results_Promise) Result() AuthenticateResponse return AuthenticateResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } +type TunnelServer_reconnectTunnel_Params struct{ capnp.Struct } + +// TunnelServer_reconnectTunnel_Params_TypeID is the unique identifier for the type TunnelServer_reconnectTunnel_Params. +const TunnelServer_reconnectTunnel_Params_TypeID = 0xa353a3556df74984 + +func NewTunnelServer_reconnectTunnel_Params(s *capnp.Segment) (TunnelServer_reconnectTunnel_Params, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) + return TunnelServer_reconnectTunnel_Params{st}, err +} + +func NewRootTunnelServer_reconnectTunnel_Params(s *capnp.Segment) (TunnelServer_reconnectTunnel_Params, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) + return TunnelServer_reconnectTunnel_Params{st}, err +} + +func ReadRootTunnelServer_reconnectTunnel_Params(msg *capnp.Message) (TunnelServer_reconnectTunnel_Params, error) { + root, err := msg.RootPtr() + return TunnelServer_reconnectTunnel_Params{root.Struct()}, err +} + +func (s TunnelServer_reconnectTunnel_Params) String() string { + str, _ := text.Marshal(0xa353a3556df74984, s.Struct) + return str +} + +func (s TunnelServer_reconnectTunnel_Params) Jwt() ([]byte, error) { + p, err := s.Struct.Ptr(0) + return []byte(p.Data()), err +} + +func (s TunnelServer_reconnectTunnel_Params) HasJwt() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s TunnelServer_reconnectTunnel_Params) SetJwt(v []byte) error { + return s.Struct.SetData(0, v) +} + +func (s TunnelServer_reconnectTunnel_Params) Hostname() (string, error) { + p, err := s.Struct.Ptr(1) + return p.Text(), err +} + +func (s TunnelServer_reconnectTunnel_Params) HasHostname() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s TunnelServer_reconnectTunnel_Params) HostnameBytes() ([]byte, error) { + p, err := s.Struct.Ptr(1) + return p.TextBytes(), err +} + +func (s TunnelServer_reconnectTunnel_Params) SetHostname(v string) error { + return s.Struct.SetText(1, v) +} + +func (s TunnelServer_reconnectTunnel_Params) Options() (RegistrationOptions, error) { + p, err := s.Struct.Ptr(2) + return RegistrationOptions{Struct: p.Struct()}, err +} + +func (s TunnelServer_reconnectTunnel_Params) HasOptions() bool { + p, err := s.Struct.Ptr(2) + return p.IsValid() || err != nil +} + +func (s TunnelServer_reconnectTunnel_Params) SetOptions(v RegistrationOptions) error { + return s.Struct.SetPtr(2, v.Struct.ToPtr()) +} + +// NewOptions sets the options field to a newly +// allocated RegistrationOptions struct, preferring placement in s's segment. +func (s TunnelServer_reconnectTunnel_Params) NewOptions() (RegistrationOptions, error) { + ss, err := NewRegistrationOptions(s.Struct.Segment()) + if err != nil { + return RegistrationOptions{}, err + } + err = s.Struct.SetPtr(2, ss.Struct.ToPtr()) + return ss, err +} + +// TunnelServer_reconnectTunnel_Params_List is a list of TunnelServer_reconnectTunnel_Params. +type TunnelServer_reconnectTunnel_Params_List struct{ capnp.List } + +// NewTunnelServer_reconnectTunnel_Params creates a new list of TunnelServer_reconnectTunnel_Params. +func NewTunnelServer_reconnectTunnel_Params_List(s *capnp.Segment, sz int32) (TunnelServer_reconnectTunnel_Params_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}, sz) + return TunnelServer_reconnectTunnel_Params_List{l}, err +} + +func (s TunnelServer_reconnectTunnel_Params_List) At(i int) TunnelServer_reconnectTunnel_Params { + return TunnelServer_reconnectTunnel_Params{s.List.Struct(i)} +} + +func (s TunnelServer_reconnectTunnel_Params_List) Set(i int, v TunnelServer_reconnectTunnel_Params) error { + return s.List.SetStruct(i, v.Struct) +} + +func (s TunnelServer_reconnectTunnel_Params_List) String() string { + str, _ := text.MarshalList(0xa353a3556df74984, s.List) + return str +} + +// TunnelServer_reconnectTunnel_Params_Promise is a wrapper for a TunnelServer_reconnectTunnel_Params promised by a client call. +type TunnelServer_reconnectTunnel_Params_Promise struct{ *capnp.Pipeline } + +func (p TunnelServer_reconnectTunnel_Params_Promise) Struct() (TunnelServer_reconnectTunnel_Params, error) { + s, err := p.Pipeline.Struct() + return TunnelServer_reconnectTunnel_Params{s}, err +} + +func (p TunnelServer_reconnectTunnel_Params_Promise) Options() RegistrationOptions_Promise { + return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} +} + +type TunnelServer_reconnectTunnel_Results struct{ capnp.Struct } + +// TunnelServer_reconnectTunnel_Results_TypeID is the unique identifier for the type TunnelServer_reconnectTunnel_Results. +const TunnelServer_reconnectTunnel_Results_TypeID = 0xd4d18de97bb12de3 + +func NewTunnelServer_reconnectTunnel_Results(s *capnp.Segment) (TunnelServer_reconnectTunnel_Results, error) { + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) + return TunnelServer_reconnectTunnel_Results{st}, err +} + +func NewRootTunnelServer_reconnectTunnel_Results(s *capnp.Segment) (TunnelServer_reconnectTunnel_Results, error) { + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) + return TunnelServer_reconnectTunnel_Results{st}, err +} + +func ReadRootTunnelServer_reconnectTunnel_Results(msg *capnp.Message) (TunnelServer_reconnectTunnel_Results, error) { + root, err := msg.RootPtr() + return TunnelServer_reconnectTunnel_Results{root.Struct()}, err +} + +func (s TunnelServer_reconnectTunnel_Results) String() string { + str, _ := text.Marshal(0xd4d18de97bb12de3, s.Struct) + return str +} + +func (s TunnelServer_reconnectTunnel_Results) Result() (TunnelRegistration, error) { + p, err := s.Struct.Ptr(0) + return TunnelRegistration{Struct: p.Struct()}, err +} + +func (s TunnelServer_reconnectTunnel_Results) HasResult() bool { + p, err := s.Struct.Ptr(0) + return p.IsValid() || err != nil +} + +func (s TunnelServer_reconnectTunnel_Results) SetResult(v TunnelRegistration) error { + return s.Struct.SetPtr(0, v.Struct.ToPtr()) +} + +// NewResult sets the result field to a newly +// allocated TunnelRegistration struct, preferring placement in s's segment. +func (s TunnelServer_reconnectTunnel_Results) NewResult() (TunnelRegistration, error) { + ss, err := NewTunnelRegistration(s.Struct.Segment()) + if err != nil { + return TunnelRegistration{}, err + } + err = s.Struct.SetPtr(0, ss.Struct.ToPtr()) + return ss, err +} + +// TunnelServer_reconnectTunnel_Results_List is a list of TunnelServer_reconnectTunnel_Results. +type TunnelServer_reconnectTunnel_Results_List struct{ capnp.List } + +// NewTunnelServer_reconnectTunnel_Results creates a new list of TunnelServer_reconnectTunnel_Results. +func NewTunnelServer_reconnectTunnel_Results_List(s *capnp.Segment, sz int32) (TunnelServer_reconnectTunnel_Results_List, error) { + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) + return TunnelServer_reconnectTunnel_Results_List{l}, err +} + +func (s TunnelServer_reconnectTunnel_Results_List) At(i int) TunnelServer_reconnectTunnel_Results { + return TunnelServer_reconnectTunnel_Results{s.List.Struct(i)} +} + +func (s TunnelServer_reconnectTunnel_Results_List) Set(i int, v TunnelServer_reconnectTunnel_Results) error { + return s.List.SetStruct(i, v.Struct) +} + +func (s TunnelServer_reconnectTunnel_Results_List) String() string { + str, _ := text.MarshalList(0xd4d18de97bb12de3, s.List) + return str +} + +// TunnelServer_reconnectTunnel_Results_Promise is a wrapper for a TunnelServer_reconnectTunnel_Results promised by a client call. +type TunnelServer_reconnectTunnel_Results_Promise struct{ *capnp.Pipeline } + +func (p TunnelServer_reconnectTunnel_Results_Promise) Struct() (TunnelServer_reconnectTunnel_Results, error) { + s, err := p.Pipeline.Struct() + return TunnelServer_reconnectTunnel_Results{s}, err +} + +func (p TunnelServer_reconnectTunnel_Results_Promise) Result() TunnelRegistration_Promise { + return TunnelRegistration_Promise{Pipeline: p.Pipeline.GetPipeline(0)} +} + type ClientService struct{ Client capnp.Client } // ClientService_TypeID is the unique identifier for the type ClientService. @@ -4041,239 +4302,249 @@ func (p ClientService_useConfiguration_Results_Promise) Result() UseConfiguratio return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } -const schema_db8274f9144abc7e = "x\xda\xccZ{p\x1d\xe5u?g\xf7\xca+)\x96" + - "\xef\xdd\xacl\xb0\xb0\xb8\xae\xc7n\xb0\x03.Fq\x0b" + - "j'W\x0f\xdbH\x8al\xdf\xd5\xc3\x80\xb0;^\xdd" + - "\xfbIZy\xef\xee\xf5>l\xc9c\xe2\xc7\xd8\x05T" + - "\x1cl\x82;\x98\x98\x8c\xed\xc6\xc5\xa6N\x83\x89\x996" + - "\x14\x98\xa4\xed\x14h\xd2\x12R\xe8\xc4)\x9d\x12\x8c\xa7" + - "\x13\xa6\x0c\x05\xd2a\xe8`\xb6s\xf6\xad+!\xd9\x99" + - "\xfe\xd1\x7f\xec\x9ds\xcf\xf78\xaf\xdfy|\xba\xf5\xe5" + - "\x9a\x16nU\xd5\xcey\x00\xf2\x99\xaa9.[\xf1\xb3" + - "]'\x96\xfd\xed~\x90\x1b\x10\xdd\xaf?\xdfU\xff\x89" + - "\xbd\xff_\xa1\x8a\x17\x00\x9a~K\xd8\x85\xd2jA\x00" + - "\x90V\x09\xff\x01\xe8V\xfd\xf6\x1bo\x95\xdf\x12\x0e\x80" + - "\xd8\x90d\xe6\x88y~u\x17J\xcb\xab\x89yY\xf5" + - "N@\xf7\x0fJ\xaf\x9e\xfa\xdd\xa3?&f.f\x06" + - "l:T\xbd\x0b\xa5\x93\x1e\xe7\x13\xd5\x1b\x01\xdd\x8f\x1e" + - "\xbd\xfe\xcfO\xfe\xd3\xcb\x07A\xfc\x12Bp\xf6\xb3\xd5" + - "\xbf@@\xe9\x95\xea\xef\x01\xba\xff|\xeb\xeew\xb6~" + - "t\xe4\x81\xc9\xe7\xa6\x88O\xad\x99@i_\x8d\x00\xbc" + - "\xfb\xc4\xbd\xf5\xff\x80'>>\x02\xe2M\xb4\x0d\xd2\xcf" + - "[jj9@\xc9\xa9\xc9\x01\xba\xaf\xde\xfc\xfcs\x87" + - "\xbf\x7f\xff\xb7@\xfe\x12\"\xf8\xeb\x8f\xd6\xfc\x0f\x9ds" + - "\xd6c\xf8\xe0;_N}\xf7\xd5/~\xdbcpO" + - "\xbfv\xd73\x87\xbf\xbf\xf8]\xe8\xe7\x04L\x014\xbd" + - "Vc\x12\xef\xbf\xd5\x90.\x1e\xfd\xf9\x0b\x1bJG\x1e" + - "?\xe5_\xda\xdb\xeb\x85Z\x8e\x83\x94\xbb\xfa\x17\x977" + - "\xae\x7ff\xe8\xc9@\x1c\xef\x1e\xe7k\x9f\xa1\xa5?\xaa" + - "\xa5c^\xde\x99y\xa8\xf5\xf7\x1e~\xb2R\xe9U\xc4" + - "y\xb9v\x02\xa5+\xb5\xf4\xf9I\xed]\x08\xe8N\xdc" + - "\xf1\xc2\xa6\x8f\xfe\xc8z\x0a\xe4[0\xe5\xfe\xdd\x83\x97" + - "v,?;\xf4\x92w+\x1e\xa0i\xfb\xdc\x9f\xd1\xd6" + - "\x07\xe7\x92\xa6\xea\xfez\xc5\x86\x87\xdf\xe9>O['" + - "\xb4\xee_bY]3J\xab\xeb<{\xd6\x11\xf7O" + - "o\xde\xf4\xe2\x8bO\x0f\x9f\xaf\xbc\x88g\xd0\xd7\xea\xba" + - "P\xba\xecq\xff\xd2\xe3\x9e\xdf\x89o\xfepU\xea/" + - "\x93f::\xef]O}\xf3\x88\xe1\xdeO\x9f\xfd\x9b" + - "\xb5\xef\xbf\xfe\x83\xa4\x01:\xd3\x1c\x19`K\x9a\x04\x1f" + - "\x98\xc0\xd2\x9b\xcd-/\x82|\x13\xa2;zt\xb7\xdd" + - "\xf1\xd8!\x17\xfaQ@\x0e\xa0\xe9`z\x17mv$" + - "M\xee\xd3\xf8^[\x9d\xfe\xfe\xfe\x1fV\xf8\x9aw\xea" + - "{\xe9.\x940CW\xbb\x92\xfe\x1e\xe0\xc7O\xdd\x7f" + - "\xb8\xf3\xd2\x9a\x97\xe4\x06LU\x0a},\xb3\x0b\xa5\xef" + - "\x12o\xd3\xd9L\x96\xf4\x19i\xb0\x82\xdd\x93\xfa\xa28" + - "\x8a\xd2{\"}\xfeJ\xf4\xd8\xbb\xee\xfd\xe6#U\x97" + - "\xbf\xf9R\xa5J).\x9a\xfe\xfb\x8b&J5\x12}" + - "VIOr\x80n\xc3\xd3\xbf\xff\x17m\xc5\x8b?\x9e" + - "&F\xa4\x0f\xe6\x7f(]\x99O_\x9f\xcc'\x19\xef" + - "\xff\xf2\xf8\xae\x0d\xcb&\xde\xa8\xd4\xbfw\xf1\xfe\x05\x13" + - "(\x95\x16\x10\xb7\xba\x80\xb8\xb9\xcb\xca\xc2\xbd\xff\xf2\xd5" + - "7\x13\x1ewq\xc1\xdb\x08)w\xc3\xa6{Gk\xee" + - "\xbbt)\xe9q\xff\xb8\xc0\xb3\xcc/\x17\x90\xe2/\x88" + - "\x8fH\xcf\x9f\xfc\xb3w\xe8 \xa1R\x9bx\xdd\x00J" + - "\xf3\xaf\xa3O\xf1:O\x86\xc8\xf3\xa7\xf3\x8b\xba\x86f" + - "\x94\x1a\x1b\xe8^\x0b\x1b\xe8^\xab\xb7\xb6\xb2\xcd\xb7\xdf" + - "\xfd.\x88\x0d\xfc\xa4@\xbf\x878U\xe2lb\x0dw" + - "\xa2\xd4z\x83\x00\xe0~cx\xe0\x95\x0f\xdaO\xfeW" + - "\xe5\xe6\x9e@\xcbohF\xe9\x0e\xe2kZ}\x83\xa7" + - "\xfe\xa6U\x7f\xfc\xde\xd1?m\xff`\xca\xee\xfb\x16\xb5" + - "\xa1td\x11\xdd\xe3\xd0\xa2;\xa5\x17\x16y\x9b\x7f}" + - "\xcd\xc6;\x96\xfc\xe8\xc3\xa4&N/\xfa\x904\xf1\x83" + - "E\xa4\x89\xa1\xdb\xff\xf3\xcee\xdf\xf8\xfb\x0f+\xcc\xe3" + - "1^\\\xb4\x02\xa5_y;^&\xe6\xf7\xd7}\xfb" + - "\xf5\x86t\xc3\xaf\xa7\xbbhM\xe3(J\x8d\x8d\xf4\xb9" + - "\xb0\xd1\xbb\xe8\xddo?\xbe3\xf7\xad_\x7fLr\xf1" + - "\x15(\xd5y\xe3\x00J[n\xa4\x9d\xef\xb9\x91B\xa5" + - "\xfb\xdc\xc5\xaf\x8e\x1c}\xf9\x93iq\xb7*\xbb\x1f\xa5" + - "\x85Y\xe2\x9e\x9f%\xac\xf9\x13\xe1\xf8\xa5\xbd\xff\xfe\x87" + - "\x9f&\xa5\xba\x92}\x9b\xa4\x12\x17\x93T\xbb\xdf?\xd6" + - "\xf1\xf0\xe6s\x9f%\x19V-~\x8e\x18Z=\x86(" + - "\xd6\xa6\xf34eq\x1bJ\xdb\x17\xd3y\xa5\xc59\xb8" + - "\xc5\xb5\x1d]g\x9aYN\x15~'\xfc,\xac,(" + - "e\xbd\xdc\xdc\xea\xd8#L\xb7\xd5\x82b\xb3\x1e\x96\xb3" + - "\xca\x86n\xb1<\xa2\x9c\xe1S\x00)\x04\x10\x95Q\x00" + - "y+\x8f\xb2\xc6\xa1\x88XOX+\xaaD\x1c\xe1Q" + - "\xb69\x149\xae\x9e\x02^\xdc\xbe\x04@\xd6x\x94\xc7" + - "8D\xbe\x9e\xe0Lt\x1e\x01\x90\xc7x\x94\x0fp\xe8" + - "\x96\x99YRt\xa6C\xda^k\x9a8\x178\x9c\x0b" + - "\xe8\x9a\xcc6\xc7\x95A\x0d\xd2,A\x16Fw\xdaX" + - "\x07\x1c\xd6\x01\xba#\x86cZ\xfd\xba\x8d\xaa\xd6\xc3\x86" + - "Lf\xe1\x08\xce\x01\x0e\xe7\x00\xce$^\xbb\xa1\xeb\xac" + - "`\xf7:\x85\x02\xb3,\x00\x92\xac:\x92l\xf9\xe3\x00" + - "\xf2\xcd<\xca\xb7'$[M\x92}\x85G\xb9\x85C" + - "\xd7b\xe6\x0efv\x1bXPl\xd5\xd07(|\x89" + - "E\xd7.h*\xd3\xedv\x03\xd2\xfa\x90:\x8c\x998" + - "\x14\x0013\xf3\xc5\xd6\x8e\xa9\x96\xad\xea\xc3}\x1e=" + - "\x9774\xb50N\xb7\x9b\xebi\xb2\xb1\x99\xf6\x10\xe7" + - "\x0f\x00 '\x8am\x009uX7L\xe6\x16U\xab" + - "@B\x01_\xb0\xf7\x0c*\x9a\xa2\x17Xt\xd0\x9c\xa9" + - "\x07\xf9\x07\xf4zr\xacT\x12\xd6^\x9aWL\x85/" + - "Y\xf2\xdcH\x1fk\x07\x00\xe45<\xca\xf9\x84>\xd6" + - "w\x01\xc8\xdd<\xcaw',\xdd\xdf\x06 \xe7y\x94" + - "7s\xe8\x1a\xa6:\xac\xea\xed\x0cx3i0\xcb\xd6" + - "\x95\x12\x03\x80Pa{\x8c2)\xd1\xc2L\x0c\xc2\x15" + - "\x9a\xaa\x9a*@\x07\xd34\xe3.\xc3\xd4\x8a\x1b\xfds" + - "\x0c\xd2\xb6g\xcah\x990\x8d\xe5=\xe3\x90\xdcj\x81" + - "\xadt,\xe6\xafsL\xcf\x90K{\x98\xe5h\xb6\x05" + - " \xa7\"\xf1\xeb\x9a\x01\xe4j\x1e\xe5z\x0es\xa6\xc7" + - "\x80\x99\x18\xd4+\xae:\x9b\xae\x1d\xddd\xc3\xaae3" + - "\xd3'/\xcd\x91\xc2KV\xf2@\xf2\xbf\x0c\x8f\xf2\"" + - "\x0e\xddaS)\xb0<3Q5\x8a\x1b\x14\xdd\xe8\xe5" + - "Y\x01\xab\x80\xc3\xaa\x99=i\x9d\xa2j\xac\xe8K\xb7" + - "\xb2\x90\xf5\xfe\xa7\xe8\x9d\xeb\xba~\xf8\x0e\xc4\xe1[\x87" + - "\x9f\xb9A\xfc\xee\x8a\xe3\xb7\x8e\xbb\xe2N\x0d\xe0:\xfe" + - "S7\x08a\x8a\x08\x9bGy/E\x84S&\x9dZ" + - "\xc0\x1b&fb\x94\x0c\xb4\xc3\x8a\xc3\xa4i\x1dr\xac" + - "@\x8a\xc6L\x98\xcc}\x06\xa1h\x8c`&\xaeT\x82" + - "e&\xdb\xc1L\x8b\xe5!m\x1ac\xe3\x98\x89\x93z" + - "\x85\xd6\xeb\xaeU\xeb\xa1\xa1\xa3U3\xaf/\xf8\x80\xb1" + - "4\x9f\x9db,\xd2\xe3\\\x1e\xe5\xeb\x09\xc8\xe8Wf" + - "\x93\xcb\x93;G\x15\xe0\xec\xee\xdcN\xff\x06\xb0\x94\x0f" + - "v1\x03d\xba>:\xec\x18\x1d\xf6\x18\x8f\xf2w\x12" + - "\x91x\xd2\x04\x90O\xf0(\x9f\xe3\x10\x83@<{\x0a" + - "@>\xc7\xa3\xfcW\x1c\x8a<\xe7\x1b\xec\xd9\x15\x00\xf2" + - "\xd3<\xca?\xe1PL\xf1\xf5T\xed\x8a\xaf\x90\xb3\xfd" + - "\x84G\xf9\xe7\x1c\x8aU\xa9z\xac\x02\x10\xdf\x18\x04\x90" + - "_\xe7Q~\xeb\xf3\xe2\xb8\xa0\x19NqHS k" + - "\xb2b\xe7\x9a\x88\xae;\xa5\xbc\xc9v\xa8h8V\xab" + - "m\xb3\x92P\xb6\xad\x10\x92\xd3\xb62l\xe1<\xc0<" + - "\x8f\x98\x89k(@\"F{\xa2\xc9\x8a\x9b\x98i\xa9" + - "\xbc\xa1G\xa8\xaa\xea6\xd3\xedn\x05\x84A\xa6E\xd4" + - "\x19\xa2\xae'\xf0\x1d\xf2\x9c \x0c\x8c\x18)p\x98\x00" + - "n\x91\xeb\x06J\\K\xbai\xe1Q\xee\xe6\xb0\x11?" + - "#2\xe9\xb1\xb3\x07@\xee\xe0Q\xee\xe3\xb0\x91\xbbB" + - "d\xd2\xa4<\x10\xe3\\z\xc4\xb6\xcb\x98\x89\x8b\xaf\xc0" + - "\xd8;\xd9\xa0e\x14\xb61@\x82\x8b\xa8\x12\x08~\x1d" + - "\x09\xe0\x0bx\xad\x88\x99\xb8\xf5\xa9\xf0\x14\xfe\xf3rW" + - "\x8e2\xa5az\xa9!\x06\xea\xdbb!B\xef\xe8\x1c" + - "\x88%\x10\xb9\x16_,y0\xbe\x7f\xb6\xa08\x16\x9b" + - "\x9ct[\x87l\xe0\x99\x19\xe1\x8c5b8Z\xb1\x87" + - "\x81`\x9b\xe3\x88\xc0!\xce\x8c>k\x8c\x8e\x84\xe2}" + - "7\x9e>\xa1D\xf9d \x99O\x02\xf5\xf7\x93\xfa\xfb" + - "x\x94\xcb\x1c\xba\x1a\xc5\xaf\xdea\x00o\xd9\xd1u}" + - "b\xde\xf0\x9cS\x00\x0e\x05@\xd7)[\xb6\xc9\x94\x12" + - "`\xe4m\xc4?\xef\x1a`\xba\x02.\xf2J\xda\x8b\xfb" + - "\xffOI\xf1\xda\xb3\x9b\x9fi&\xe5\xb6S\x89TS" + - "\x08V\xa3\xb7\xbc\xdd\xd0\x85k\xae_\x02\x04\xf3\xd1u" + - "e\x90-\xa9\xb4\x0a\xd3\xcerJ%Ky\x94oM" + - "\xa6\x9d[HE7\xf1(\x7f\x85C\x81\x99\x94A\xa2" + - "\x16\xd7?t\x8f\xe5\xd7j\x98\x89\xc7\x13\xb3_'Q" + - "\xc6\xaa\x86>\xc5\x0d\x97\xc4\xe1\x12\x99\xb0\xf3\xb6\x84]" + - "C\x13\xae\x1f\x8c\xed*lc\xe3\xa1\x95\xb2\xac\xa4\xa8" + - "1\x1a\x05\xc6m\x05\xe1k1\xcf\x8c\xe5^\x90\x16\xfd" + - "\xa4\x98\xf3\xadE\x97\xac\x8f.y\xdf\x04\x80\xbc\x97G" + - "\xf9\xa1\xc4%\x1f\xa4\xea\xf9!\x1e\xe5\xc7\x12\x97\xe4\x9f&\xc8?\xc3\xa3|\x81\xf3\x00" + - "\xbb\xa3\xb5\xdd\xd01\xb8\x84\x05\x10U\xd0#L1\xed" + - "A\xa6\xa0\xdd\xa9\xdb\xcc\xdc\xa1\xa0\x16B\xc2\x1e[-" + - "1\xc3\xb1#\x88()c^\xc9\x81\xc5\x0e\x7f\x95\xa0" + - "\xd8\x16\xd6\x00\x875\x14\x91\x163\xdbMVD\xb2\x86" + - "\xa2\xe5\x15\xde\x1e\xb9\x1a\x05M\x06\xf1\xf44\xea\xa1\x82" + - "e7\x8f\xf2\x03\x04%\x98\x18\xa3\x88\x07G\x81\xf3\x90" + - "\x84d\xde\xde\x16\x970^B\xac\xaahB\xbc\x848" + - "\x07@\xdcG\xda9\xc0\xa3|\x98\x0b\xaf\xd6a@\xce" + - "\x8f\xd0JS\x07E\xfe\x1eBM\x95\xc5\xf2\x06\xf5\x82" + - "\x8a\x86\xde\xe7)\x0acM\x15\x8cR\xd9$WV\x0d" + - "]v\x14M\xe5\xed\xf1h\xe1\x8c\xba H\xf2Cy" + - "c9\xeb\x19\x8b\x94qk\xa8\x0c\xa9\x15\xbb\x00z[" + - "\x90\xc7\xden\x8c\xddE\xea\xc46\x80\xde5D\xcfc" + - "\xec1\xd2zl\x00\xe8\xed z\x1fF\xbd\x99$\xe3" + - "S\x00\xbd}D\xde\x8aq\xa9 m\xf1\xb6\xdfL\xf4" + - "\x11\x8c\xab\x05\x89\xe1\x0a\x80\xde\xadD\xdfM\xf49\x9c" + - "\xa7Ii\x1cG\x01z\xc7\x88~\x80\xe8BU=5" + - "\xa2\xd2>4\x01z\xf7\x12\xfd!\xa2W__\x8f\xd5" + - "\x00\xd2\x83\x1e\xfd\x01\xa2?J\xf4\x9a\x85\xf5X\x03 " + - "\x1d\xc1\xfd\x00\xbd\x87\x89~\x9c\xe8\xb5X\x8f\xb5\x00\xd2" + - "1|\x1c\xa0\xf78\xd1\xcf\x10\xfd\x0bs\xea\xf1\x0b\x00" + - "\xd2i\xef>'\x88~\x0e#\\\xeb,&\xe1\x95\xdc" + - "J\x8d\xcb\x0b\xde\xb0\"\xd3\xb2\xa0\x07C\x1f\xfb\xf3F" + - "\x9a\x9a0L\xc7\xa3N@L\x03\xbae\xc3\xd06L" + - "\x86\xed\xd9*\x9c\xc0- m\xe8\x9d\xc5(\xce|g" + - "\xea6 [P\xb4\xcer\\\xf3X\xad\x8em8e" + - "\xc8\x16\x15\x9b\x15\xa3\xc4k:\xfa:\xd3(\xf5!3" + - "K\xaa\xaeh\x10\xfd2\x93o\xa5\x1dG-N\x09:" + - "\xae\xd2\xd1\xb2\xe5\xe6>e\xb8\xa2#^\x11\xa3v\x04" + - "B\xb7\xdc\x16\x83v:\x19\x1c\xd9\x1d\x8a\xe6\xb0)'" + - "MS\xf5\xf6Wd(?oL\xe9\xc7\xdb\xe2\xd3\xa3" + - "\xc3\xcd\xa0G\xef\xe0\xe2\xdc\x10ja(\xe8} K" + - "{'\xec\x11M\xbc\x02{\\m50\xccl\xff\xab" + - "S\x1f2(\x8d\x0aJ\xc9\xfa\x0dW\xf70+M\xad" + - "\xc7\xac\x1df4\xc3\x9a=\xcfu\xf4\xf5\xe5\xe36\x98" + - "\xf7A2\x89\x0b=I\\\x88aa4\x19\xfeau" + - "(\xc9^\x1c\xe6\x89\xbe\x19\xe3\xfeA\xba\x07OM\x8a" + - "\xffT\xab\x8f\x0b\xcc\xdb\xbeH\xf4\xb2\x87\x0b\xe8\xe3B" + - "\xc9\xdb_#\xfaX\x12\x17\x1c\x9c\x98\x8c\x0b|\x88\x0b" + - "\x14\xcf\x07\x88~\xd8\xc3\x85\x94\x8f\x0b\x87\xf0\x99I\xf1" + - "_S\xe5\xe3\xc21|nR\xfc\xd7\xce\xf1q\xe1\xb4" + - "\xc7\x7f\x86\xe8\x17<\\h\xf3q\xe1\xbc\x87#O\x13" + - "\xfdy\xc2\x05\xc7\xd4zmS\xd5\x01\x87cg-\x94" + - "\xbf\xc6X\xb9\x15\xd2\x9a\xba\x83E\x98]T\x15m\x8d" + - "\xa3h\x90\xed\xb5\x95\xc2\xb6\xb8\x04\xd6\xac\x0eE/Z" + - "8\xa2lc\x84\xf4B2'\xda\x9a\xb5\x89\x99\xea\x10" + - "`\\4G5B:o\x18\x95\xa5\x83W{1\xd3" + - "\x07\x95\xe8\xb7\x922\xd6Y\xd4X;\x86\x95\x02\xaf\xc7" + - "\x99F\xa5_\x0c]G?}\xf7\xa9\xd9\xc9y\xb9\x1c" + - "\x94\xe1a~\xef\xcbU$n6Vf\x05\xbb\xdd@" + - "\xddVu\x87M\xd9\xa00\xe2\xe8\xdbXq-\xea\x05" + - "\xa3\xa8\xea\xc30\xa5\xfe\xe7?o\xfa\x90(h\xbch" + - "\xc6\xc4\xdb\x8b\xb8\xbc\x198\x0fK(=\x8b\xcdq\x17" + - "\x9d+x\xabr&S\xacD\x038\xc3i\xc1\xb4\xcc" + - "\x0f2\xbf>\xa8\x02\x88^20\x1c\x17\x8bGv\x01" + - "'>(`\x9a\x97\x18\x1fa\xd8\x8a\x0a\x93\xb2Ar" + - "\x984o\x96\xbe&\xd9Xy\xe9)\xe5\xb9X\xf8\x16" + - "\x86\xe1\xab\xa4(\x92\xab\xd4\x09n\xd8|a\x98\xdb\xc8" + - "\x9aI\x93]c\x07\xda\xc3\xb2\xd6\xd5\xa4\x8d\xf05c" + - "\xf6A\x82\x7fN\x9a\xbc\xcf\x17(\xdaw41\xd2\xd2" + - "\x8c\xa0yJoH\xd6\xbf3\xe8\xca\xbfpX\xad\xa6" + - "iq\x85\xfb-I\x94\xdd\x91\xff-\x89\x0b\xf9\xa8\xa5" + - "9\xd8\x158\xe5\x89\xf8\xe5\xe0\x89\x89x\xdc\x15\xb9\xdf" + - "\xd9\xae\xb8\xa5\xf1\x1a\xc8\xf0\xb1\xc01c\x8c\xd5\x8c\xe1" + - "nUg\x16\xd5k\x15c\x81\xf0\x05\x02mB.\xc7" + - "$\xf8\x9d\x0cs\x9dk\x12e\xdeL\xe2\xf7\x06\xce\xee" + - "\xfbz\x90\x8b\x13M\xe7\xa9\xc4<&\x14^~.\x98" + - "slM\x08\xbf\x85\x9a\xce\xcd<\xca#\x9c\x17\xfcF" + - "\x7f\xb9\xa8\xa0\xcd\xd6\x99l\xbb\xc3\x04\xbd0\x1e7_" + - "\xd4~\x14\xac~,S\xa1\xb8\xced\xb9\xed\x0eK2" + - "\x84Sd\x10T\xa38e|\x11\x1b|\xfaT\xfc\x7f\x93=\x7f\xa3G\x10*" + - "D\x85\xab)\xd2\xa2\xbf\xac\x98}\xc4;\xed@\xb9'" + - "\xc7\xae*\xac\xe3\xa7\xbdk\x1c\xc2@\x14\xdb\x98x\x99" + - "\xa7C\xb8`\xf3\xff\x0d\x00\x00\xff\xff\xdfs7\x9d" +const schema_db8274f9144abc7e = "x\xda\xc4Z}\x90\x14ez\x7f\x9e\xee\x19\x9a\x85]" + + "f\xda^\x8b=\x04\x16(\x8d\xc2\x09Q\xd1D7\xf1" + + "f?\x80\xdb\xe5\xf8\x98\xdeYPWL\xd1\xcc\xbc\xbb" + + "\xdb\xd0\xd3=t\xf7\x00K\xf0\x10\x0a\xe3\xb9\x91\x13<" + + "I\x89\x07W\x80\x1a?\xc2\xe5\xc0\x83\x8a\x1a\xce:\x13" + + "\x8d\x98\xbb\x8b\xde\x05R\xc7\xc5\xaa3\"\x95\xd2\x0ae" + + "\xd4\xb3,Sj\xa7\x9e\xb7?wv]\xc0\\U\xfe" + + "\x81\xa9\xa7\x9f\xf7\xe3\xf9\xfa=\x1f\xef^\xd7;\xa1U" + + "\xb8>}u\x06@=\x92\x1e\xe7\xb19\xbf\xda|\xe0" + + "\xaa\x7f\xdc\x0e\xea\x14D\xef\xdb'\x167~\xean\xff" + + "wH\x8b\x12\xc0\xfcAi3*;%\x09@\xb9_" + + "\xfaO@/\xfd\x07\xa7\xdf\xaa\xbc%\xed\x00yJ\x92" + + "Y \xe6\xf2\xf8\xc5\xa8l\x1bO\xccw\x8f\xdf\x08\xe8" + + "\xfdi\xf9\xf5C\x7f\xb4\xe7g\xc4,\xc4\xcc\x80\xf3\xcf" + + "\x8d\xdf\x8c\xca\xa7\x9c\xf3\xe3\xf1\xcb\x01\xbd\x8f\x1en\xfa" + + "\x9b\x83\xffr\xf2^\x90\xafF\x08\xcen\xa8\xfb\x0d\x02" + + "*3\xeb~\x04\xe8\xfd\xebu[\xdeY\xfd\xd1\xee\xef" + + "\x0c?7E|/\xd5\x0d\xa1r\xa6N\x02\xd1\xdb\x7f" + + "g\xe3?\xe3\x81Ov\x83|\x0dm\x83\xf4\xf9x\xdd" + + "\x04\x01P\xf9E]\x0e\xd0{\xfd\xda\x13/\xec\xfa\xf1" + + "}\xdf\x07\xf5jD\xf0\xd7\x9f\xaf\xfb\x1f:\x07'\x10" + + "\xc3\x07\x8f\x7f=\xf5\xc3\xd7/\xfb\x01g\xf0\x9e\xf8\xe5" + + "m\xcf\xee\xfa\xf1\x8c\xf7`\x85 a\x0a`\xfe\xec\x09" + + "6\xf1\xde4\x81t\xf1\xf0\xaf\x7f\xb2\xac\xbc\xfb\xd1C" + + "\xfe\xa5\xf9^\x97O\x14\x04Hy;\xba>)\xafx" + + "\xac\xf0XR\x9c\xba\x89\x1f\xd2\xd2i\x13I\x9c\x9b~" + + "sn\xf9\xd2g\xfb\x9e\x0c\x18\xf8E\x9f\x9f\xf8,1" + + "\xbc6\x91\xeeqrc\xf6\x81\xb6?~\xf0\xc9Z\xab" + + "\xa4\xf9\x8d'\x0e\xa1\x92\xae\xa7\x9fX\x7f\x1b\x02zC" + + "\xb7\xfcd\xe5G\x7f\xe1<\x03\xea\\Ly/\xdf\x7f" + + "v\xc3\xec\xa7\xfb^\xe5\xd7\x16\xc9\x8c\x0d\xbf\xa2\xadw" + + "6\xd0\xd9\x0d\x7f?g\xd9\x83\xef,9J['\xcc" + + "\xe2_b\xee\xa4\x16Tn\x9dD\x96\xb9e\x12q\xbf" + + "q\xed\xca\x17_<\xd2\x7f\xb4\xf6\"\xdc\xe2g&-" + + "F\xe5<\xe7~\x97s_\xde\x85o\xfe\xf4\xfa\xd4\xdf" + + "%\x05\xdf\x9fy\x8f\x0e?\x9a!\x86;?;\xfe\x0f" + + "\x0b\xdf?\xf5|\xd2BjV \x0b\xb1,\x09\xde;" + + "\x84\xe57[Z_\x04\xf5\x1aDo\xed\x9e-n\xe7" + + "#;=X\x81\x12\x0a\x00\xf3wf7\xd3f{\xb3" + + "\xe4_\xd3\xce\xb77\x98\xefo\xffi\x8d3\xf2S?" + + "\xce.F\xa5N\xa6\xab\xa5\xe5\x1f\x01~\xf2\xcc}\xbb" + + "\xba\xce.xU\x9d\x82\xa9Z\xa1\x0f\xca\x9bQ9N" + + "\xbc\xf3\x8f\xca\xcd\xa4\xcfH\x835\xec\\\xea\xff\xb8l" + + "-*\x1f_F??\xb8\x8c\xb3/\xbe\xf3{\x0f\xa5" + + "\xcf}\xef\xd5Z\x95R\xe0\xcc\xff\\\xb1Q\x91\x1b\xb9" + + "K7\xfeV\x00\xf4\xa6\x1c\xf9\x93\xbfm/\x9d\xf9\xd9" + + "(A\xa4\xdc:\xf9C\xa5k2\xfdZ8\x99d<" + + ";\xf7\xe8\x9f\xbf\xbb\xf3\x97\xa7\x92\x9erp2\xf7\xd8" + + "\xe3\x93Ia\xf7}}p\xf3\xb2\xab\x86N\xd7\x1a\x88" + + "s\x9e\x9e<\x84\xcay\xbe\xdd\xbb|;\xe1\x9c\xf6\xb5" + + "{\xfe\xed\x1bo&|\xf6\x8e\xa6\xb7\x11R\xde\xb2\x95" + + "w\xae\xad\xbb\xfb\xec\xd9\xe4AK\x9b\xb8\xe9\xb4&:" + + "\xe8\x98\xfc\x90r\xe2\xe0_\xbfC\x07I\xb5\xea\xde\xd6" + + "\xd4\x8b\xca\x9e&\xfa\xb9\xbb\xe9I\x122\x8a\x9d\xd1\x1c" + + "g\xe7\x15-\xa8\xec\xbf\x82\xee\xb5\xf7\x0a\xba\xd7M\xab" + + "\xdb\xd8\xaa\x9bo\x7f\x0f\xe4)\xe20\xa88C\x9c\xef" + + "\x12\xe7\xfcsW\xdc\x87\xca\xf9\xa9\x12\x80\xf7\xdd\xfe\xde" + + "\xd7>\xe88\xf8\xdf\xb5\x9bs\x81NOmA\xe5\xdc" + + "Tn\xaa\xa9\xdc>\xf3\xaf\xff\xcb\xf3{\x1e\xeb\xf8`" + + "\xc4\xees\xa7\xb7\xa3r\xebt\xee\xee\xd3\xbf\xa9\xe8\xd3" + + "\xf9\xe6\xdf^\xb0\xfc\x96Y/}\x98\xd4\x84:\x9dG" + + "/\x9bN\x9a\xe8\xbb\xf9\xbf\xbey\xd5w\xff\xe9\xc3\x1a" + + "\xfbq\xc6{\xa7\xcfAe\x0f\xdfq71\xbf\xbf\xe8" + + "\x07\xa7\xa6d\xa6\xfcn\xb4\x8b\x1e\x9f\xbe\x16\x95\xd7\x88" + + "w\xfe+\xd3y\x1c\xdf\xfe\xf6\xa3\x1bs\xdf\xff\xdd'" + + "$\x97X\x83s\xb7\xcc\xe8Ee\xe9\x0c\xda\xb9k\x06" + + "\xc5\xd2\x92\xc3g\xbe1\xb0\xe7\xe4\xa7\xa3\"\xf7\xc73" + + "\xb6\xa3R7\x93\xfb\xffLB\xab\xbf\x92\xf6\x9d\xbd\xe7" + + "\xb7\x7f\xf6YR\xaa\xf33\xdf\xe6\xd07\x8b\xa4\xda\xf2" + + "\xfe\xde\xce\x07W\x1d\xfe\"\xc90s\xd6\x0b\xc4p=" + + "g\x88\x82q4OSg\xb5\xa3\xa2\xcd\xa2\xf3\xee\x9a" + + "\x95\x83\xb9\x9e[5Mf\xd8\x95T\xf1\x0f\xc3\x9f\xc5" + + "yE\xadbVZ\xda\xaa\xee\x003]\xbd\xa8\xb9\xac" + + "\x9b\xe5\x9c\x8ae:,\x8f\xa8f\xc5\x14@\x0a\x01d" + + "m-\x80\xbaZD\xd5\x10PFl$\xb4\x96u\"" + + "\x0e\x88\xa8\xba\x02\xca\x82\xd0H\x88 \xaf\x9f\x05\xa0\x1a" + + "\"\xaa\x9b\x04D\xb1\x91\xf0N\xae>\x04\xa0n\x12Q" + + "\xdd!\xa0WavY3\x99\x09\x19w\xa1mc=" + + "\x08X\x0f\xe8\xd9\xcc\xb5\x07\xb55\x06dX\x82,\xad" + + "\xdd\xe8b\x03\x08\xd8\x00\xe8\x0dXU\xdbYa\xba\xa8" + + "\x1b\xdd\xac\xcff\x0e\x0e\xe08\x10p\x1c\xe0X\xe2u" + + "X\xa6\xc9\x8an\xa1Z,2\xc7\x01 \xc9\xc6G\x92" + + "\xcd~\x14@\xbdVD\xf5\xe6\x84d7\x91d7\x8a" + + "\xa8\xb6\x0a\xe89\xcc\xde\xc0\xec%\x16\x165W\xb7\xcc" + + "e\x9aXf\xd1\xb5\x8b\x86\xceL\xb7\xc3\x82\x8c\xd9\xa7" + + "\xf7c6\x0e\x05@\xcc\x8e}\xb1\x85\x9bt\xc7\xd5\xcd" + + "\xfe\x1eN\xcf\xe5-C/\x0e\xd2\xed\xea\xb9&\xa7\xb5" + + "\xd0\x1e\xf2\xe5\xbd\x00(\xc8r;@N\xef7-\x9b" + + "y%\xdd)\x92P \x16\xdd\xadk4C3\x8b," + + ":h\xdc\xc8\x83\xfc\x03\x0a\\\x8eyZ\xc2\xdaW\xe6" + + "5[\x13\xcb\x8eZ\x1f\xe9ca/\x80\xba@D5" + + "\x9f\xd0\xc7\xd2\xc5\x00\xea\x12\x11\xd5\xdb\x13\x96^\xd1\x0e" + + "\xa0\xe6ETW\x09\xe8Y\xb6\xde\xaf\x9b\x1d\x0cD;" + + "i0\xc75\xb52\x03\x80Pa[\xad\x0a)\xd1\xc1" + + "l\x8c\xd25\x9aJ\x8f\x14\xa0\x93\x19\x86u\x9be\x1b" + + "\xa5\xe5\xfe9\x16i\x9b\x9b2Z&\x8dbyn\x1c" + + "\x92[/\xb2yU\x87\xf9\xeb\xaa67\xe4\x95\xdd\xcc" + + "\xa9\x1a\xae\x03\xa0\xa6\"\xf1\x1bZ\x00\xd4\xf1\"\xaa\x8d" + + "\x02\xe6l\xce\x80\xd9\x18\xd4k\xaez!]WM\x9b" + + "\xf5\xeb\x8e\xcbl\x9f|e\x8e\x14^v\x92\x07\x92\xff" + + "eET\xa7\x0a\xe8\xf5\xdbZ\x91\xe5\x99\x8d\xbaUZ" + + "\xa6\x99VAdEL\x83\x80\xe9\xb1=i\x91\xa6\x1b" + + "\xac\xe4K7\xaf\xd8\xcc\xff\xa7\xe8\xad\xf7\xe9R\xb5\x1e\x1a:Z5\xf6" + + "z\x9b\x15}\xc8\x08\x96\xe7\x9b}\xa3%\x82\x84t\xd4" + + "*\xa2\xba$\x11$]\x14$\x9d\"\xaa=\x89 Q" + + "\xdb\xe3\xc8\xa9\x01\xb3\xdfSl\x0c\xbbzp\xf1\xe8\xca" + + "\x09?#\x17\xa8\x17Qm\"\x0c\xa6\xaf\xcc\xa5h\xa5" + + "\xd3\xa2\xea\xf6\xc2\xa7u\xd0\xbf\x01\xa2\xe6\x83]\xec\x00" + + "T\x9b\xa2\xc3\xf6\xd2a\x8f\x88\xa8>\x9e\xd0\xcfA\x1b" + + "@= \xa2zX@\x0c\xd4\xf3\xf4!\x00\xf5\xb0\x88" + + "\xeas\x02\xca\xa2\xe0\xfb\xda\xf19\xd4\x0b\x89\xa8\xfe\\" + + "@9%6R\xa9/\xbfFq\xf2s\x11\xd5_\x0b" + + "(\xa7S\x8d\x98\x06\x90O\xaf\x01PO\x89\xa8\xbe\xf5" + + "e\x10T4\xacj\xa9\xcf\xd0\xa0\xd9f\xa5\xae\x05\x11" + + "\xdd\xac\x96\xf36\xdb\xa0\xa3Uu\xda\\\x97\x95\xa5\x8a" + + "\xeb\x84\xd9$\xe3j\xfd\x0eN\x02\xcc\x8b\x88\xd9\xb8>" + + "\x04$b\xb4'\xda\xac\xb4\x92\xd9\x8e.Zf\x94\x10" + + "t\xd3e\xa6\xbbD\x03i\x0d3\"\xea\x18\x80\xd1\x1d" + + "\xb8=9}\x10\xc1V\x0cr\xd8On7\xd5\xf3\x02" + + "%.\x9c\x13{\xde4\xfc\x82\xc8\xa4\xc7\xae\xee\xd8\xf7" + + "\xa6\x09\x9f\x13\x994\xa9\xf6\xc6\x10\x9d\x19p\xdd\x0af" + + "\xe3\xba10\xf6F\xb6\xc6\xb1\x8a\xeb\x18 !]T" + + "\xc4\x04_\x07\x02\xe4\x05\xd1(a6\xee\xfbj\xc2\xfc\xa0E\xd8K\x06\xde'\xa2\xfa\x94" + + "\x80\x98\xf2!\xff\x09\x82\xfc\xa7DT\x8f\x09\x1c\xb0;" + + "\xdb:,\x13\x83K8\x00Q\xf1?\xc04\xdb]\xc3" + + "4t\xbbL\x97\xd9\x1b44BH\xd8\xea\xeaef" + + "U\xdd\x08\"\xca\xda&^-a\xa9\xd3_%i\xae" + + "\x83u `\x1dE\xa4\xc3\xec\x0e\x9b\x95\x90\xac\xa1\x19" + + "yMt\x07.FA\xc3A<3\x8az\xa8\xd6\xda" + + "\"\xa2\xfa\x1d\x82\x12L\x8c\x88\xe4{\xd7\x82\xc0\x91\x84" + + "d^\xdf\x1eW_'\x88~\x92\xe8\xf5\xa9F\xac\x07P^\xc1C" + + "\x00\x85\x93D?\x85\x11\xdeu\x95\x92\xb0K\xee\xa6\xc7" + + "e\x87h9\x91\xc9Y\xd0V\xa2\x9f\x13\xf2V\x86\xfa" + + "J\xcc\xc4\xf3_@\xcc\x00z\x15\xcb2\x96\x0d\x87\xf3" + + "\x0bU>\x81\xbb@\xc62\xbbJQ\xfc\xf9N\xb6\xc4" + + "\x82\xe6\xa2ftU\xe2Z\xc8i\xab\xbaV\xb5\x02\xcd" + + "%\xcde\xa5(!\xdbUs\x91m\x95{\x90\xd9e" + + "\xdd\xd4\x0c\x88\xbe\x8c\xe5s\x99jU/E{\x8fY" + + "\xc0E\xee)\xd4\xbags\xa5\xa5G\xeb\xaf\x19\x01\xcc" + + "\x89\xb1>\x82\xae\xb97\xc4P\x9fI\x86T\xf3\x06\xcd" + + "\xa8\xb2\x8b\xa9\xec\xc6l*\xbas~Sr\xa1\xde3" + + "\x1cX]\xb84_Q\x93F\xfd\xe46b\xde\xd1\x1e" + + "\x0b\x1b\xc9j\x073\x90N!N`\xa1I\xfa\x82\xde" + + "\x12\x9ai\xef\x84sD\x13\xc5\xc09.V\x13\xfd\xcc" + + "\xf5\x7fu\x99}\x16\xe5zI+;_qu7s" + + "2\x17\xa3\xc5xFx\xe1d\xdc\xd9\xd3\x93\x8f\xc7\x0c" + + "\xa2\x8f\xe4\xd7E\xe0\xd5\x86\xdd\x00\x85V\x8a\xce%\x18" + + "\xe9P\xe9\xe2 \xd2I\xe4\x1e\x8cKXE\xe5`\x91" + + "'\xfa*\x8c\x9b\x1c\xe5\x0e\x1e\xe41\x06\xa6\xda|\xf0" + + "b|\xfb\x08\xeb\xe44\xfa\xe0U\xe6\xfb\x1bD\xdf\x94" + + "\x04\xaf*\x0e\x0d\x03;I\xf4\xc1k\x1b\x07\x9d\x1dD" + + "\xdf\xc5\xc1+\xe5\x83\xd7N|\x16\xa0\xb0\x8b\xe8\xfb8" + + "x\xa5}\xf0\xda\x8b/\x0c\x03\xbb\x09\xe3|\xf0z\x82" + + "\xf3?E\xf4c\x1c\xbc\xda}\xf0:\xca\xc1\xee\x08\xd1" + + "O\x10HUm\xa3\xe0\xda\xba\x09\xd8\x1f\xc7F\xb1\xf2" + + "-\xc6*m\x901\xf4\x0d,J,%]3\x16T" + + "5\x03\x9a\x0b\xaeV\\\x17\xd7\xe9\x86\xd3\xa9\x99%\x07" + + "\x07\xb4u\x8c\xd2\x91\x94L\xdc\xae\xe1\xacd\xb6\xde\x07" + + "\x18W\xf6Q!\x93\xc9[Vm}\xc3\x0bDf\xfb" + + "\x08\x17}+k\x9b\xbaJ\x06\xeb\xc0\xb0\x9c\x11\xcd8" + + "\x1d\xea\xf4\xc52M\xf4k\x8c\x1e\xbdyx\xf1P\x09" + + "z\x85\xb0\x08\xe9\xc9\xd5T\x17lS\x85\x15\xdd\x0e\x0b" + + "MW7\xabl\xc4\x06\xc5\x81\xaa\xb9\x8e\x95\x16\xa2Y" + + "\xb4J\xba\xd9\x0f#\x9a\x14\xf1\xcb\xa6;\x89\xaa\x8bG" + + "3&^\xc7\xe4\xd9- p\xe8\xa2\x1aBn\x89[" + + "\xfd\\\x91\xaf\xca\xd9Ls\x12]\xea\x18\xa7\x05\xd3H" + + "?\xc8\xfc\xb6>\x0d\x10=%a8\x8e\x97\x8fn\x06" + + "A~Z\xc2\xf8\x15\x03\xc3G\x0by\xbf\x0d\x82\xbcG" + + "B!z\xe2\xc3\xf0yN\xbe\x7f\x08\x04\xf9^\x09\xc5" + + "\xe8\xd9\x0d\xc3Y\xb7<\xd8\x0e\x82\\\x960\x15=A" + + "b8(\x975\xaa\x93\xee\x900\x1d\xbd\xe7a\xf8\x1a" + + "#/\xdd\x0e\x82\xbcP\xf2\xc2v\x08r\xbe\x18\xad\xe8" + + "\x85\x80\x01\xcd\x1c2Z\xd1\x0b\xe7C\x18\xb6M\x00\xad" + + "\xb85\x80\xe7V\xf4\xc2\x09)d\x8a\x9a\xcbZ\xa9\xd7" + + "\xf4?b\x00\xde\xd0\x8a\xc9\xc9\xa3\xf8e\x0d\xce\xe8\x85" + + "r{\\\xcc\x85\x00\xbcm(\xae\xe5\xa2\xa6r\xe73" + + "\xc9:9\x98\x8d\xec\xdd\x1eLV\x8e%f#G\xa9" + + "x>&\xa2\xfa\x86\x10W\x06\xa1O\x87\xc3:\xb4\xec" + + "\xb0\xcb\x1dcf\x17x~P\xc3\xd6N\xee\xbc\x925" + + "\xc0k\\\xf4\xb7r N\x07\xc9q\xde\xa4\xc48\x0f" + + "\xc3\xfeZ\x1a\x96=\x92\xc3\xbdI\x17h\xd6\x92\xdd\"" + + "Og)\xee\x92\xe1\xe3%\x86\xef\xcc\xb2L\xae\xd5 " + + "yaG\x89a.\x84\x1a\x93]b[\xdd\xcd\x9a\xff" + + "/\xc9z\x14\x07\xf1\xcf\xc9\x90G\xfa\x02E\xfb\xaeM" + + "\xcc\xe9\x0c+\xe8\x083\xcb\x92E\xfd\x18\xba\xf2/\x1c" + + "\x96\xe0\x19Z\\3\x9a\xa3f\xf2a\x11\xd5\x03\x89b" + + "g\xff\xac\xc4\xbc.\xec\xd3\x0e.\x0e\xe6u'\xe2\x97" + + "\x9c\xe7\xc9QO\x88\xa8\x9eL\xb8\xdf+\xc4\xf8\xb2\xef" + + "~a\xbe\x92\x7fA=\xcb\x1b\"\xaao\x06\xadr\xf8" + + "\xa2S\xb5c\xa06\xac\xfe%\xba\xc9\x1c\xaa@k\x06" + + " \xe13\x11\xba\x04\x7fU\x9b0|8Vv-H" + + "\x14\xae\xd1<\x08\x99]\xa0p-\xa1\x13\xcdY\xc6\xd0" + + "W!\x88\x0e?8\x82d\x9fh\xbd\x0f%\xa6R\xa1" + + "\xb6\xd4\x17\x82i\xcf\xea\x84\xb6\xee\xa2\xd6{\x95\x88\xea" + + "\x80\xc0\x11\xc4ZQ)i\xe8\xb2E6[_e\x92" + + "Y\x1c\x8c[Pj\xc2\x8a\xce\x0a\xacPY\xbc\xc8f" + + "\xb9\xf5U\x96d\x08\x9f\x01@\xd2\xad\xd2\x88\xf9\xff(" + + "\xa5\xdfmlM\xc1*\xaec\xee\xb0\xe7\x91\x9a'\xbc" + + "\xee\xf8\x0d z\xc1\xebN\xbe\xe0\x05\xb8\xb3\x9e<\xb0" + + "\"\xa2\xba%\x81;\x83Cq\xff:z\xae\xff\xfd\xa4" + + "\xe7\xaf\xf4\x8aE\x95\xaet1U`\xf4\xc75_q" + + "\xac~\xb1E{\xfc6{\x89\xa3(\x88\xc0\x00\x13\x7f" + + "{A\x87\x08\xc1\xe6\xff\x1b\x00\x00\xff\xff\x99N\xd0U" func init() { schemas.Register(schema_db8274f9144abc7e, @@ -4286,6 +4557,7 @@ func init() { 0x9b87b390babc2ccf, 0x9e12cfad042ba4f1, 0xa29a916d4ebdd894, + 0xa353a3556df74984, 0xa766b24d4fe5da35, 0xa78f37418c1077c8, 0xaa7386f356bd398a, @@ -4299,6 +4571,7 @@ func init() { 0xc766a92976e389c4, 0xc793e50592935b4a, 0xcbd96442ae3bb01a, + 0xd4d18de97bb12de3, 0xd58a254e7a792b87, 0xdc3ed6801961e502, 0xe3e37d096a5b564e, From 7173da9359300525a8e7316bdefdbffb3eb9fece Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Mon, 18 Nov 2019 17:01:20 -0600 Subject: [PATCH 05/21] TUN-2567: AuthOutcome can be turned back into AuthResponse --- tunnelrpc/pogs/auth_outcome.go | 25 +++++++++++++++++++++++++ tunnelrpc/pogs/auth_test.go | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/tunnelrpc/pogs/auth_outcome.go b/tunnelrpc/pogs/auth_outcome.go index 9f72711e..56001f10 100644 --- a/tunnelrpc/pogs/auth_outcome.go +++ b/tunnelrpc/pogs/auth_outcome.go @@ -43,6 +43,8 @@ func (ar AuthenticateResponse) Outcome() AuthOutcome { //go-sumtype:decl AuthOutcome type AuthOutcome interface { isAuthOutcome() + // Serialize into an AuthenticateResponse which can be sent via Capnp + Serialize() AuthenticateResponse } // AuthSuccess means the backend successfully authenticated this cloudflared. @@ -56,6 +58,14 @@ func (ao *AuthSuccess) RefreshAfter() time.Duration { return hoursToTime(ao.HoursUntilRefresh) } +// Serialize into an AuthenticateResponse which can be sent via Capnp +func (ao *AuthSuccess) Serialize() AuthenticateResponse { + return AuthenticateResponse{ + Jwt: ao.Jwt, + HoursUntilRefresh: ao.HoursUntilRefresh, + } +} + func (ao *AuthSuccess) isAuthOutcome() {} // AuthFail means this cloudflared has the wrong auth and should exit. @@ -63,6 +73,13 @@ type AuthFail struct { Err error } +// Serialize into an AuthenticateResponse which can be sent via Capnp +func (ao *AuthFail) Serialize() AuthenticateResponse { + return AuthenticateResponse{ + PermanentErr: ao.Err.Error(), + } +} + func (ao *AuthFail) isAuthOutcome() {} // AuthUnknown means the backend couldn't finish checking authentication. Try again later. @@ -76,6 +93,14 @@ func (ao *AuthUnknown) RefreshAfter() time.Duration { return hoursToTime(ao.HoursUntilRefresh) } +// Serialize into an AuthenticateResponse which can be sent via Capnp +func (ao *AuthUnknown) Serialize() AuthenticateResponse { + return AuthenticateResponse{ + RetryableErr: ao.Err.Error(), + HoursUntilRefresh: ao.HoursUntilRefresh, + } +} + func (ao *AuthUnknown) isAuthOutcome() {} func hoursToTime(hours uint8) time.Duration { diff --git a/tunnelrpc/pogs/auth_test.go b/tunnelrpc/pogs/auth_test.go index 50ae1ec1..3a5590e1 100644 --- a/tunnelrpc/pogs/auth_test.go +++ b/tunnelrpc/pogs/auth_test.go @@ -58,9 +58,13 @@ func TestAuthenticateResponseOutcome(t *testing.T) { Jwt: tt.fields.Jwt, HoursUntilRefresh: tt.fields.HoursUntilRefresh, } - if got := ar.Outcome(); !reflect.DeepEqual(got, tt.want) { + got := ar.Outcome() + if !reflect.DeepEqual(got, tt.want) { t.Errorf("AuthenticateResponse.Outcome() = %T, want %v", got, tt.want) } + if got != nil && !reflect.DeepEqual(got.Serialize(), ar) { + t.Errorf(".Outcome() and .Serialize() should be inverses but weren't. Expected %v, got %v", ar, got.Serialize()) + } }) } } From 1ba5abfdb33ddafcb56b8534402f238d599ae203 Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Mon, 18 Nov 2019 15:35:06 -0600 Subject: [PATCH 06/21] Release 2019.11.1 --- RELEASE_NOTES | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index ea7fe24a..55feba36 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,3 +1,12 @@ +2019.11.1 +- 2019-11-12 Add db-connect, a SQL over HTTPS server +- 2019-11-12 TUN-2053: Add a /healthcheck endpoint to the metrics server +- 2019-11-13 TUN-2178: public API to create new h2mux.MuxedStreamRequest +- 2019-11-13 TUN-2490: respect original representation of HTTP request path +- 2019-11-18 TUN-2547: TunnelRPC definitions for Authenticate flow +- 2019-11-18 TUN-2551: TunnelRPC definitions for ReconnectTunnel flow +- 2019-11-05 TUN-2506: Expose active streams metrics + 2019.11.0 - 2019-11-04 TUN-2502: Switch to go modules - 2019-11-04 TUN-2500: Don't send client registration errors to Sentry From c5bacf4d9526edea484053e884a959f71a5d39d2 Mon Sep 17 00:00:00 2001 From: Chung-Ting Huang Date: Mon, 18 Nov 2019 18:46:51 -0600 Subject: [PATCH 07/21] TUN-2563: Exposes config_version metrics --- supervisor/supervisor.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/supervisor/supervisor.go b/supervisor/supervisor.go index 28b32092..7ae111df 100644 --- a/supervisor/supervisor.go +++ b/supervisor/supervisor.go @@ -16,6 +16,7 @@ import ( "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/streamhandler" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" + "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" ) @@ -28,6 +29,27 @@ type Supervisor struct { useConfigResultChan chan<- *pogs.UseConfigurationResult state *state logger *logrus.Entry + metrics metrics +} + +type metrics struct { + configVersion prometheus.Gauge +} + +func newMetrics() metrics { + configVersion := prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: "supervisor", + Subsystem: "supervisor", + Name: "config_version", + Help: "Latest configuration version received from Cloudflare", + }, + ) + prometheus.MustRegister( + configVersion, + ) + return metrics{ + configVersion: configVersion, + } } func NewSupervisor( @@ -70,6 +92,7 @@ func NewSupervisor( useConfigResultChan: useConfigResultChan, state: newState(defaultClientConfig), logger: logger.WithField("subsystem", "supervisor"), + metrics: newMetrics(), }, nil } @@ -131,6 +154,7 @@ func (s *Supervisor) notifySubsystemsNewConfig(newConfig *pogs.ClientConfig) *po Success: true, } } + s.metrics.configVersion.Set(float64(newConfig.Version)) s.state.updateConfig(newConfig) var tunnelHostnames []h2mux.TunnelHostname From 8ee1faf3172dd002c8bfbd4c83aa827ac59713bd Mon Sep 17 00:00:00 2001 From: Chung-Ting Huang Date: Tue, 19 Nov 2019 15:16:00 -0600 Subject: [PATCH 08/21] Release 2019.11.2 --- RELEASE_NOTES | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 55feba36..001463ef 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,3 +1,7 @@ +2019.11.2 +- 2019-11-18 TUN-2567: AuthOutcome can be turned back into AuthResponse +- 2019-11-18 TUN-2563: Exposes config_version metrics + 2019.11.1 - 2019-11-12 Add db-connect, a SQL over HTTPS server - 2019-11-12 TUN-2053: Add a /healthcheck endpoint to the metrics server From 871c3a194fdeca0567fdc308474c4f89446b8b4b Mon Sep 17 00:00:00 2001 From: Chung-Ting Huang Date: Wed, 20 Nov 2019 13:48:33 -0600 Subject: [PATCH 09/21] TUN-2562: Update Cloudflare Origin CA RSA root --- tlsconfig/cloudflare_ca.go | 44 +++++++++++++++++++------------------- validation/validation.go | 3 ++- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/tlsconfig/cloudflare_ca.go b/tlsconfig/cloudflare_ca.go index e70cc503..8c4b1a9a 100644 --- a/tlsconfig/cloudflare_ca.go +++ b/tlsconfig/cloudflare_ca.go @@ -26,28 +26,28 @@ mcifak4CQsr+DH4pn5SJD7JxtCG3YGswW8QZsw== -----END CERTIFICATE----- Issuer: C=US, O=CloudFlare, Inc., OU=CloudFlare Origin SSL Certificate Authority, L=San Francisco, ST=California -----BEGIN CERTIFICATE----- -MIID/DCCAuagAwIBAgIID+rOSdTGfGcwCwYJKoZIhvcNAQELMIGLMQswCQYDVQQG -EwJVUzEZMBcGA1UEChMQQ2xvdWRGbGFyZSwgSW5jLjE0MDIGA1UECxMrQ2xvdWRG -bGFyZSBPcmlnaW4gU1NMIENlcnRpZmljYXRlIEF1dGhvcml0eTEWMBQGA1UEBxMN -U2FuIEZyYW5jaXNjbzETMBEGA1UECBMKQ2FsaWZvcm5pYTAeFw0xNDExMTMyMDM4 -NTBaFw0xOTExMTQwMTQzNTBaMIGLMQswCQYDVQQGEwJVUzEZMBcGA1UEChMQQ2xv -dWRGbGFyZSwgSW5jLjE0MDIGA1UECxMrQ2xvdWRGbGFyZSBPcmlnaW4gU1NMIENl -cnRpZmljYXRlIEF1dGhvcml0eTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzETMBEG -A1UECBMKQ2FsaWZvcm5pYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AMBIlWf1KEKR5hbB75OYrAcUXobpD/AxvSYRXr91mbRu+lqE7YbyyRUShQh15lem -ef+umeEtPZoLFLhcLyczJxOhI+siLGDQm/a/UDkWvAXYa5DZ+pHU5ct5nZ8pGzqJ -p8G1Hy5RMVYDXZT9F6EaHjMG0OOffH6Ih25TtgfyyrjXycwDH0u6GXt+G/rywcqz -/9W4Aki3XNQMUHNQAtBLEEIYHMkyTYJxuL2tXO6ID5cCsoWw8meHufTeZW2DyUpl -yP3AHt4149RQSyWZMJ6AyntL9d8Xhfpxd9rJkh9Kge2iV9rQTFuE1rRT5s7OSJcK -xUsklgHcGHYMcNfNMilNHb8CAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgAGMBIGA1Ud -EwEB/wQIMAYBAf8CAQIwHQYDVR0OBBYEFCToU1ddfDRAh6nrlNu64RZ4/CmkMB8G -A1UdIwQYMBaAFCToU1ddfDRAh6nrlNu64RZ4/CmkMAsGCSqGSIb3DQEBCwOCAQEA -cQDBVAoRrhhsGegsSFsv1w8v27zzHKaJNv6ffLGIRvXK8VKKK0gKXh2zQtN9SnaD -gYNe7Pr4C3I8ooYKRJJWLsmEHdGdnYYmj0OJfGrfQf6MLIc/11bQhLepZTxdhFYh -QGgDl6gRmb8aDwk7Q92BPvek5nMzaWlP82ixavvYI+okoSY8pwdcVKobx6rWzMWz -ZEC9M6H3F0dDYE23XcCFIdgNSAmmGyXPBstOe0aAJXwJTxOEPn36VWr0PKIQJy5Y -4o1wpMpqCOIwWc8J9REV/REzN6Z1LXImdUgXIXOwrz56gKUJzPejtBQyIGj0mveX -Fu6q54beR89jDc+oABmOgg== +MIIEADCCAuigAwIBAgIID+rOSdTGfGcwDQYJKoZIhvcNAQELBQAwgYsxCzAJBgNV +BAYTAlVTMRkwFwYDVQQKExBDbG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91 +ZEZsYXJlIE9yaWdpbiBTU0wgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQH +Ew1TYW4gRnJhbmNpc2NvMRMwEQYDVQQIEwpDYWxpZm9ybmlhMB4XDTE5MDgyMzIx +MDgwMFoXDTI5MDgxNTE3MDAwMFowgYsxCzAJBgNVBAYTAlVTMRkwFwYDVQQKExBD +bG91ZEZsYXJlLCBJbmMuMTQwMgYDVQQLEytDbG91ZEZsYXJlIE9yaWdpbiBTU0wg +Q2VydGlmaWNhdGUgQXV0aG9yaXR5MRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRMw +EQYDVQQIEwpDYWxpZm9ybmlhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAwEiVZ/UoQpHmFsHvk5isBxRehukP8DG9JhFev3WZtG76WoTthvLJFRKFCHXm +V6Z5/66Z4S09mgsUuFwvJzMnE6Ej6yIsYNCb9r9QORa8BdhrkNn6kdTly3mdnykb +OomnwbUfLlExVgNdlP0XoRoeMwbQ4598foiHblO2B/LKuNfJzAMfS7oZe34b+vLB +yrP/1bgCSLdc1AxQc1AC0EsQQhgcyTJNgnG4va1c7ogPlwKyhbDyZ4e59N5lbYPJ +SmXI/cAe3jXj1FBLJZkwnoDKe0v13xeF+nF32smSH0qB7aJX2tBMW4TWtFPmzs5I +lwrFSySWAdwYdgxw180yKU0dvwIDAQABo2YwZDAOBgNVHQ8BAf8EBAMCAQYwEgYD +VR0TAQH/BAgwBgEB/wIBAjAdBgNVHQ4EFgQUJOhTV118NECHqeuU27rhFnj8KaQw +HwYDVR0jBBgwFoAUJOhTV118NECHqeuU27rhFnj8KaQwDQYJKoZIhvcNAQELBQAD +ggEBAHwOf9Ur1l0Ar5vFE6PNrZWrDfQIMyEfdgSKofCdTckbqXNTiXdgbHs+TWoQ +wAB0pfJDAHJDXOTCWRyTeXOseeOi5Btj5CnEuw3P0oXqdqevM1/+uWp0CM35zgZ8 +VD4aITxity0djzE6Qnx3Syzz+ZkoBgTnNum7d9A66/V636x4vTeqbZFBr9erJzgz +hhurjcoacvRNhnjtDRM0dPeiCJ50CP3wEYuvUzDHUaowOsnLCjQIkWbR7Ni6KEIk +MOz2U0OBSif3FTkhCgZWQKOOLo1P42jHC3ssUZAtVNXrCk3fw9/E15k8NPkBazZ6 +0iykLhH1trywrKRMVw67F44IE8Y= -----END CERTIFICATE----- Issuer: C=US, O=CloudFlare, Inc., OU=Origin Pull, L=San Francisco, ST=California, CN=origin-pull.cloudflare.net -----BEGIN CERTIFICATE----- diff --git a/validation/validation.go b/validation/validation.go index 83857230..97233864 100644 --- a/validation/validation.go +++ b/validation/validation.go @@ -190,10 +190,11 @@ func ValidateHTTPService(originURL string, hostname string, transport http.Round _, secondErr := client.Do(secondRequest) if secondErr == nil { // Worked this time--advise the user to switch protocols return errors.Errorf( - "%s doesn't seem to work over %s, but does seem to work over %s. Consider changing the origin URL to %s", + "%s doesn't seem to work over %s, but does seem to work over %s. Reason: %v. Consider changing the origin URL to %s", parsedURL.Host, oldScheme, parsedURL.Scheme, + initialErr, parsedURL, ) } From 9605f00c771dda19c785c7e13364fa39854ab34b Mon Sep 17 00:00:00 2001 From: Chung-Ting Huang Date: Wed, 20 Nov 2019 16:33:38 -0600 Subject: [PATCH 10/21] Release 2019.11.3 --- RELEASE_NOTES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/RELEASE_NOTES b/RELEASE_NOTES index 001463ef..5a874a17 100644 --- a/RELEASE_NOTES +++ b/RELEASE_NOTES @@ -1,3 +1,6 @@ +2019.11.3 +- 2019-11-20 TUN-2562: Update Cloudflare Origin CA RSA root + 2019.11.2 - 2019-11-18 TUN-2567: AuthOutcome can be turned back into AuthResponse - 2019-11-18 TUN-2563: Exposes config_version metrics From 23e12cf5a3436f19fb7526f76773bfaa1475ee32 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Wed, 20 Nov 2019 12:12:08 -0600 Subject: [PATCH 11/21] TUN-2575: Constructors + simpler conversions for AuthOutcome --- tunnelrpc/pogs/auth_outcome.go | 58 ++++++++++++++++++++-------------- tunnelrpc/pogs/auth_test.go | 27 ++++++++++++++-- 2 files changed, 59 insertions(+), 26 deletions(-) diff --git a/tunnelrpc/pogs/auth_outcome.go b/tunnelrpc/pogs/auth_outcome.go index 56001f10..6626ed4f 100644 --- a/tunnelrpc/pogs/auth_outcome.go +++ b/tunnelrpc/pogs/auth_outcome.go @@ -21,18 +21,18 @@ func (ar AuthenticateResponse) Outcome() AuthOutcome { // If there was a network error, then cloudflared should retry later, // because origintunneld couldn't prove whether auth was correct or not. if ar.RetryableErr != "" { - return &AuthUnknown{Err: fmt.Errorf(ar.RetryableErr), HoursUntilRefresh: ar.HoursUntilRefresh} + return NewAuthUnknown(fmt.Errorf(ar.RetryableErr), ar.HoursUntilRefresh) } // If the user's authentication was unsuccessful, the server will return an error explaining why. // cloudflared should fatal with this error. if ar.PermanentErr != "" { - return &AuthFail{Err: fmt.Errorf(ar.PermanentErr)} + return NewAuthFail(fmt.Errorf(ar.PermanentErr)) } // If auth succeeded, return the token and refresh it when instructed. if ar.PermanentErr == "" && len(ar.Jwt) > 0 { - return &AuthSuccess{Jwt: ar.Jwt, HoursUntilRefresh: ar.HoursUntilRefresh} + return NewAuthSuccess(ar.Jwt, ar.HoursUntilRefresh) } // Otherwise the state got messed up. @@ -49,59 +49,71 @@ type AuthOutcome interface { // AuthSuccess means the backend successfully authenticated this cloudflared. type AuthSuccess struct { - Jwt []byte - HoursUntilRefresh uint8 + jwt []byte + hoursUntilRefresh uint8 +} + +func NewAuthSuccess(jwt []byte, hoursUntilRefresh uint8) AuthSuccess { + return AuthSuccess{jwt: jwt, hoursUntilRefresh: hoursUntilRefresh} } // RefreshAfter is how long cloudflared should wait before rerunning Authenticate. -func (ao *AuthSuccess) RefreshAfter() time.Duration { - return hoursToTime(ao.HoursUntilRefresh) +func (ao AuthSuccess) RefreshAfter() time.Duration { + return hoursToTime(ao.hoursUntilRefresh) } // Serialize into an AuthenticateResponse which can be sent via Capnp -func (ao *AuthSuccess) Serialize() AuthenticateResponse { +func (ao AuthSuccess) Serialize() AuthenticateResponse { return AuthenticateResponse{ - Jwt: ao.Jwt, - HoursUntilRefresh: ao.HoursUntilRefresh, + Jwt: ao.jwt, + HoursUntilRefresh: ao.hoursUntilRefresh, } } -func (ao *AuthSuccess) isAuthOutcome() {} +func (ao AuthSuccess) isAuthOutcome() {} // AuthFail means this cloudflared has the wrong auth and should exit. type AuthFail struct { - Err error + err error +} + +func NewAuthFail(err error) AuthFail { + return AuthFail{err: err} } // Serialize into an AuthenticateResponse which can be sent via Capnp -func (ao *AuthFail) Serialize() AuthenticateResponse { +func (ao AuthFail) Serialize() AuthenticateResponse { return AuthenticateResponse{ - PermanentErr: ao.Err.Error(), + PermanentErr: ao.err.Error(), } } -func (ao *AuthFail) isAuthOutcome() {} +func (ao AuthFail) isAuthOutcome() {} // AuthUnknown means the backend couldn't finish checking authentication. Try again later. type AuthUnknown struct { - Err error - HoursUntilRefresh uint8 + err error + hoursUntilRefresh uint8 +} + +func NewAuthUnknown(err error, hoursUntilRefresh uint8) AuthUnknown { + return AuthUnknown{err: err, hoursUntilRefresh: hoursUntilRefresh} } // RefreshAfter is how long cloudflared should wait before rerunning Authenticate. -func (ao *AuthUnknown) RefreshAfter() time.Duration { - return hoursToTime(ao.HoursUntilRefresh) +func (ao AuthUnknown) RefreshAfter() time.Duration { + return hoursToTime(ao.hoursUntilRefresh) } // Serialize into an AuthenticateResponse which can be sent via Capnp -func (ao *AuthUnknown) Serialize() AuthenticateResponse { +func (ao AuthUnknown) Serialize() AuthenticateResponse { return AuthenticateResponse{ - RetryableErr: ao.Err.Error(), - HoursUntilRefresh: ao.HoursUntilRefresh, + RetryableErr: ao.err.Error(), + HoursUntilRefresh: ao.hoursUntilRefresh, } } -func (ao *AuthUnknown) isAuthOutcome() {} +func (ao AuthUnknown) isAuthOutcome() {} func hoursToTime(hours uint8) time.Duration { return time.Duration(hours) * time.Hour diff --git a/tunnelrpc/pogs/auth_test.go b/tunnelrpc/pogs/auth_test.go index 3a5590e1..f30f3977 100644 --- a/tunnelrpc/pogs/auth_test.go +++ b/tunnelrpc/pogs/auth_test.go @@ -31,15 +31,15 @@ func TestAuthenticateResponseOutcome(t *testing.T) { }{ {"success", fields{Jwt: []byte("asdf"), HoursUntilRefresh: 6}, - &AuthSuccess{Jwt: []byte("asdf"), HoursUntilRefresh: 6}, + AuthSuccess{jwt: []byte("asdf"), hoursUntilRefresh: 6}, }, {"fail", fields{PermanentErr: "bad creds"}, - &AuthFail{Err: fmt.Errorf("bad creds")}, + AuthFail{err: fmt.Errorf("bad creds")}, }, {"error", fields{RetryableErr: "bad conn", HoursUntilRefresh: 6}, - &AuthUnknown{Err: fmt.Errorf("bad conn"), HoursUntilRefresh: 6}, + AuthUnknown{err: fmt.Errorf("bad conn"), hoursUntilRefresh: 6}, }, {"nil (no fields are set)", fields{}, @@ -69,6 +69,27 @@ func TestAuthenticateResponseOutcome(t *testing.T) { } } +func TestAuthSuccess(t *testing.T) { + input := NewAuthSuccess([]byte("asdf"), 6) + output, ok := input.Serialize().Outcome().(AuthSuccess) + assert.True(t, ok) + assert.Equal(t, input, output) +} + +func TestAuthUnknown(t *testing.T) { + input := NewAuthUnknown(fmt.Errorf("pdx unreachable"), 6) + output, ok := input.Serialize().Outcome().(AuthUnknown) + assert.True(t, ok) + assert.Equal(t, input, output) +} + +func TestAuthFail(t *testing.T) { + input := NewAuthFail(fmt.Errorf("wrong creds")) + output, ok := input.Serialize().Outcome().(AuthFail) + assert.True(t, ok) + assert.Equal(t, input, output) +} + func TestWhenToRefresh(t *testing.T) { expected := 4 * time.Hour actual := hoursToTime(4) From dd614881b6c17696e880ef11110b287f14b6d92d Mon Sep 17 00:00:00 2001 From: Khaled Elkhawaga Date: Fri, 22 Nov 2019 17:27:28 +0100 Subject: [PATCH 12/21] Fix Docker build failure (#149) * Enables module-mode in Docker to fix build * Statically compile binary in Docker to fix missing dependency errors * Ensure target OS is set to Linux for Docker builds --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 7a142e6b..89e04a66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,7 @@ FROM golang:1.12 as builder +ENV GO111MODULE=on +ENV CGO_ENABLED=0 +ENV GOOS=linux WORKDIR /go/src/github.com/cloudflare/cloudflared/ RUN apt-get update && apt-get install -y --no-install-recommends upx # Run after `apt-get update` to improve rebuild scenarios From b0d31a0ef300e370b02f2b7a187c1025bd01690f Mon Sep 17 00:00:00 2001 From: Chung-Ting Huang Date: Thu, 21 Nov 2019 10:56:04 -0600 Subject: [PATCH 13/21] TUN-2573: Refactor TunnelRegistration into PermanentRegistrationError, RetryableRegistrationError and SuccessfulTunnelRegistration --- origin/tunnel.go | 34 ++++----- tunnelrpc/pogs/tunnelrpc.go | 127 +++++++++++++++++++++++++++---- tunnelrpc/pogs/tunnelrpc_test.go | 30 ++++++-- 3 files changed, 151 insertions(+), 40 deletions(-) diff --git a/origin/tunnel.go b/origin/tunnel.go index 070f38ef..86a50a16 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -19,6 +19,7 @@ import ( "github.com/cloudflare/cloudflared/signal" "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/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/websocket" @@ -335,23 +336,21 @@ func RegisterTunnel( serverInfoPromise := tsClient.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error { return nil }) - registration, err := ts.RegisterTunnel( + LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger) + registration := ts.RegisterTunnel( ctx, config.OriginCert, config.Hostname, config.RegistrationOptions(connectionID, originLocalIP, uuid), ) - LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger) - if err != nil { + + if registrationErr := registration.DeserializeError(); registrationErr != nil { // RegisterTunnel RPC failure - return newClientRegisterTunnelError(err, config.Metrics.regFail) - } - for _, logLine := range registration.LogLines { - logger.Info(logLine) + return processRegisterTunnelError(registrationErr, config.Metrics) } - if regErr := processRegisterTunnelError(registration.Err, registration.PermanentFailure, config.Metrics); regErr != nil { - return regErr + for _, logLine := range registration.LogLines { + logger.Info(logLine) } if registration.TunnelID != "" { @@ -374,22 +373,19 @@ func RegisterTunnel( config.Metrics.userHostnamesCounts.WithLabelValues(registration.Url).Inc() logger.Infof("Route propagating, it may take up to 1 minute for your new route to become functional") + config.Metrics.regSuccess.Inc() return nil } -func processRegisterTunnelError(err string, permanentFailure bool, metrics *TunnelMetrics) error { - if err == "" { - metrics.regSuccess.Inc() - return nil - } - - metrics.regFail.WithLabelValues(err).Inc() - if err == DuplicateConnectionError { +func processRegisterTunnelError(err pogs.TunnelRegistrationError, metrics *TunnelMetrics) error { + if err.Error() == DuplicateConnectionError { + metrics.regFail.WithLabelValues("dup_edge_conn").Inc() return dupConnRegisterTunnelError{} } + metrics.regFail.WithLabelValues("server_error").Inc() return serverRegisterTunnelError{ - cause: fmt.Errorf("Server error: %s", err), - permanent: permanentFailure, + cause: fmt.Errorf("Server error: %s", err.Error()), + permanent: err.IsPermanent(), } } diff --git a/tunnelrpc/pogs/tunnelrpc.go b/tunnelrpc/pogs/tunnelrpc.go index b675f0f2..1c169554 100644 --- a/tunnelrpc/pogs/tunnelrpc.go +++ b/tunnelrpc/pogs/tunnelrpc.go @@ -9,13 +9,16 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" - log "github.com/sirupsen/logrus" capnp "zombiezen.com/go/capnproto2" "zombiezen.com/go/capnproto2/pogs" "zombiezen.com/go/capnproto2/rpc" "zombiezen.com/go/capnproto2/server" ) +const ( + defaultRetryAfterSeconds = 15 +) + type Authentication struct { Key string Email string @@ -33,14 +36,111 @@ func UnmarshalAuthentication(s tunnelrpc.Authentication) (*Authentication, error } type TunnelRegistration struct { + SuccessfulTunnelRegistration Err string - Url string - LogLines []string PermanentFailure bool - TunnelID string `capnp:"tunnelID"` RetryAfterSeconds uint16 } +type SuccessfulTunnelRegistration struct { + Url string + LogLines []string + TunnelID string `capnp:"tunnelID"` +} + +func NewSuccessfulTunnelRegistration( + url string, + logLines []string, + tunnelID string, +) *TunnelRegistration { + // Marshal nil will result in an error + if logLines == nil { + logLines = []string{} + } + return &TunnelRegistration{ + SuccessfulTunnelRegistration: SuccessfulTunnelRegistration{ + Url: url, + LogLines: logLines, + TunnelID: tunnelID, + }, + } +} + +// Not calling this function Error() to avoid confusion with implementing error interface +func (tr TunnelRegistration) DeserializeError() TunnelRegistrationError { + if tr.Err != "" { + err := fmt.Errorf(tr.Err) + if tr.PermanentFailure { + return NewPermanentRegistrationError(err) + } + retryAfterSeconds := tr.RetryAfterSeconds + if retryAfterSeconds < defaultRetryAfterSeconds { + retryAfterSeconds = defaultRetryAfterSeconds + } + return NewRetryableRegistrationError(err, retryAfterSeconds) + } + return nil +} + +type TunnelRegistrationError interface { + error + Serialize() *TunnelRegistration + IsPermanent() bool +} + +type PermanentRegistrationError struct { + err string +} + +func NewPermanentRegistrationError(err error) TunnelRegistrationError { + return &PermanentRegistrationError{ + err: err.Error(), + } +} + +func (pre *PermanentRegistrationError) Error() string { + return pre.err +} + +func (pre *PermanentRegistrationError) Serialize() *TunnelRegistration { + return &TunnelRegistration{ + Err: pre.err, + PermanentFailure: true, + } +} + +func (*PermanentRegistrationError) IsPermanent() bool { + return true +} + +type RetryableRegistrationError struct { + err string + retryAfterSeconds uint16 +} + +func NewRetryableRegistrationError(err error, retryAfterSeconds uint16) TunnelRegistrationError { + return &RetryableRegistrationError{ + err: err.Error(), + retryAfterSeconds: retryAfterSeconds, + } +} + +func (rre *RetryableRegistrationError) Error() string { + return rre.err +} + +func (rre *RetryableRegistrationError) Serialize() *TunnelRegistration { + return &TunnelRegistration{ + Err: rre.err, + PermanentFailure: false, + RetryAfterSeconds: rre.retryAfterSeconds, + } +} + +func (*RetryableRegistrationError) IsPermanent() bool { + return false +} + func MarshalTunnelRegistration(s tunnelrpc.TunnelRegistration, p *TunnelRegistration) error { return pogs.Insert(tunnelrpc.TunnelRegistration_TypeID, s.Struct, p) } @@ -325,7 +425,7 @@ func UnmarshalConnectParameters(s tunnelrpc.CapnpConnectParameters) (*ConnectPar } type TunnelServer interface { - RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) + RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) *TunnelRegistration GetServerInfo(ctx context.Context) (*ServerInfo, error) UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error) @@ -359,15 +459,12 @@ func (i TunnelServer_PogsImpl) RegisterTunnel(p tunnelrpc.TunnelServer_registerT return err } server.Ack(p.Options) - registration, err := i.impl.RegisterTunnel(p.Ctx, originCert, hostname, pogsOptions) - if err != nil { - return err - } + registration := i.impl.RegisterTunnel(p.Ctx, originCert, hostname, pogsOptions) + result, err := p.Results.NewResult() if err != nil { return err } - log.Info(registration.TunnelID) return MarshalTunnelRegistration(result, registration) } @@ -420,7 +517,7 @@ func (c TunnelServer_PogsClient) Close() error { return c.Conn.Close() } -func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) { +func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) *TunnelRegistration { client := tunnelrpc.TunnelServer{Client: c.Client} promise := client.RegisterTunnel(ctx, func(p tunnelrpc.TunnelServer_registerTunnel_Params) error { err := p.SetOriginCert(originCert) @@ -443,9 +540,13 @@ func (c TunnelServer_PogsClient) RegisterTunnel(ctx context.Context, originCert }) retval, err := promise.Result().Struct() if err != nil { - return nil, err + return NewRetryableRegistrationError(err, defaultRetryAfterSeconds).Serialize() } - return UnmarshalTunnelRegistration(retval) + registration, err := UnmarshalTunnelRegistration(retval) + if err != nil { + return NewRetryableRegistrationError(err, defaultRetryAfterSeconds).Serialize() + } + return registration } func (c TunnelServer_PogsClient) GetServerInfo(ctx context.Context) (*ServerInfo, error) { diff --git a/tunnelrpc/pogs/tunnelrpc_test.go b/tunnelrpc/pogs/tunnelrpc_test.go index 98f061e0..4ae8f2fb 100644 --- a/tunnelrpc/pogs/tunnelrpc_test.go +++ b/tunnelrpc/pogs/tunnelrpc_test.go @@ -1,6 +1,7 @@ package pogs import ( + "fmt" "reflect" "testing" "time" @@ -11,16 +12,29 @@ import ( capnp "zombiezen.com/go/capnproto2" ) +const ( + testURL = "tunnel.example.com" + testTunnelID = "asdfghjkl;" + testRetryAfterSeconds = 19 +) + +var ( + testErr = fmt.Errorf("Invalid credential") + testLogLines = []string{"all", "working"} +) + +// *PermanentRegistrationError implements TunnelRegistrationError +var _ TunnelRegistrationError = (*PermanentRegistrationError)(nil) + +// *RetryableRegistrationError implements TunnelRegistrationError +var _ TunnelRegistrationError = (*RetryableRegistrationError)(nil) + func TestTunnelRegistration(t *testing.T) { testCases := []*TunnelRegistration{ - &TunnelRegistration{ - Err: "it broke", - Url: "asdf.cftunnel.com", - LogLines: []string{"it", "was", "broken"}, - PermanentFailure: true, - TunnelID: "asdfghjkl;", - RetryAfterSeconds: 19, - }, + NewSuccessfulTunnelRegistration(testURL, testLogLines, testTunnelID), + NewSuccessfulTunnelRegistration(testURL, nil, testTunnelID), + NewPermanentRegistrationError(testErr).Serialize(), + NewRetryableRegistrationError(testErr, testRetryAfterSeconds).Serialize(), } for i, testCase := range testCases { _, seg, err := capnp.NewMessage(capnp.SingleSegment(nil)) From f51712bef974bdd02d6cd966abc32787ca5bed77 Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Fri, 22 Nov 2019 10:17:23 -0600 Subject: [PATCH 14/21] TUN-2582: EventDigest field in tunnelrpc --- tunnelrpc/pogs/tunnelrpc.go | 15 +- tunnelrpc/pogs/tunnelrpc_test.go | 9 +- tunnelrpc/tunnelrpc.capnp | 2 + tunnelrpc/tunnelrpc.capnp.go | 508 ++++++++++++++++--------------- 4 files changed, 278 insertions(+), 256 deletions(-) diff --git a/tunnelrpc/pogs/tunnelrpc.go b/tunnelrpc/pogs/tunnelrpc.go index 1c169554..d1c771f1 100644 --- a/tunnelrpc/pogs/tunnelrpc.go +++ b/tunnelrpc/pogs/tunnelrpc.go @@ -43,15 +43,17 @@ type TunnelRegistration struct { } type SuccessfulTunnelRegistration struct { - Url string - LogLines []string - TunnelID string `capnp:"tunnelID"` + Url string + LogLines []string + TunnelID string `capnp:"tunnelID"` + EventDigest []byte } func NewSuccessfulTunnelRegistration( url string, logLines []string, tunnelID string, + eventDigest []byte, ) *TunnelRegistration { // Marshal nil will result in an error if logLines == nil { @@ -59,9 +61,10 @@ func NewSuccessfulTunnelRegistration( } return &TunnelRegistration{ SuccessfulTunnelRegistration: SuccessfulTunnelRegistration{ - Url: url, - LogLines: logLines, - TunnelID: tunnelID, + Url: url, + LogLines: logLines, + TunnelID: tunnelID, + EventDigest: eventDigest, }, } } diff --git a/tunnelrpc/pogs/tunnelrpc_test.go b/tunnelrpc/pogs/tunnelrpc_test.go index 4ae8f2fb..6a1d5283 100644 --- a/tunnelrpc/pogs/tunnelrpc_test.go +++ b/tunnelrpc/pogs/tunnelrpc_test.go @@ -19,8 +19,9 @@ const ( ) var ( - testErr = fmt.Errorf("Invalid credential") - testLogLines = []string{"all", "working"} + testErr = fmt.Errorf("Invalid credential") + testLogLines = []string{"all", "working"} + testEventDigest = []byte("asdf") ) // *PermanentRegistrationError implements TunnelRegistrationError @@ -31,8 +32,8 @@ var _ TunnelRegistrationError = (*RetryableRegistrationError)(nil) func TestTunnelRegistration(t *testing.T) { testCases := []*TunnelRegistration{ - NewSuccessfulTunnelRegistration(testURL, testLogLines, testTunnelID), - NewSuccessfulTunnelRegistration(testURL, nil, testTunnelID), + NewSuccessfulTunnelRegistration(testURL, testLogLines, testTunnelID, testEventDigest), + NewSuccessfulTunnelRegistration(testURL, nil, testTunnelID, testEventDigest), NewPermanentRegistrationError(testErr).Serialize(), NewRetryableRegistrationError(testErr, testRetryAfterSeconds).Serialize(), } diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index a8110a9b..4f4948a1 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -21,6 +21,8 @@ struct TunnelRegistration { tunnelID @4 :Text; # How long should this connection wait to retry in seconds, if the error wasn't permanent retryAfterSeconds @5 :UInt16; + # A unique ID used to reconnect this tunnel. + eventDigest @6 :Data; } struct RegistrationOptions { diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index b34167dc..4e3ce630 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -125,12 +125,12 @@ type TunnelRegistration struct{ capnp.Struct } const TunnelRegistration_TypeID = 0xf41a0f001ad49e46 func NewTunnelRegistration(s *capnp.Segment) (TunnelRegistration, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 4}) + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 5}) return TunnelRegistration{st}, err } func NewRootTunnelRegistration(s *capnp.Segment) (TunnelRegistration, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 4}) + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 5}) return TunnelRegistration{st}, err } @@ -242,12 +242,26 @@ func (s TunnelRegistration) SetRetryAfterSeconds(v uint16) { s.Struct.SetUint16(2, v) } +func (s TunnelRegistration) EventDigest() ([]byte, error) { + p, err := s.Struct.Ptr(4) + return []byte(p.Data()), err +} + +func (s TunnelRegistration) HasEventDigest() bool { + p, err := s.Struct.Ptr(4) + return p.IsValid() || err != nil +} + +func (s TunnelRegistration) SetEventDigest(v []byte) error { + return s.Struct.SetData(4, v) +} + // TunnelRegistration_List is a list of TunnelRegistration. type TunnelRegistration_List struct{ capnp.List } // NewTunnelRegistration creates a new list of TunnelRegistration. func NewTunnelRegistration_List(s *capnp.Segment, sz int32) (TunnelRegistration_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 4}, sz) + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 5}, sz) return TunnelRegistration_List{l}, err } @@ -4302,249 +4316,251 @@ func (p ClientService_useConfiguration_Results_Promise) Result() UseConfiguratio return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } -const schema_db8274f9144abc7e = "x\xda\xc4Z}\x90\x14ez\x7f\x9e\xee\x19\x9a\x85]" + - "f\xda^\x8b=\x04\x16(\x8d\xc2\x09Q\xd1D7\xf1" + - "f?\x80\xdb\xe5\xf8\x98\xdeYPWL\xd1\xcc\xbc\xbb" + - "\xdb\xd0\xd3=t\xf7\x00K\xf0\x10\x0a\xe3\xb9\x91\x13<" + - "I\x89\x07W\x80\x1a?\xc2\xe5\xc0\x83\x8a\x1a\xce:\x13" + - "\x8d\x98\xbb\x8b\xde\x05R\xc7\xc5\xaa3\"\x95\xd2\x0ae" + - "\xd4\xb3,Sj\xa7\x9e\xb7?wv]\xc0\\U\xfe" + - "\x81\xa9\xa7\x9f\xf7\xe3\xf9\xfa=\x1f\xef^\xd7;\xa1U" + - "\xb8>}u\x06@=\x92\x1e\xe7\xb19\xbf\xda|\xe0" + - "\xaa\x7f\xdc\x0e\xea\x14D\xef\xdb'\x167~\xean\xff" + - "wH\x8b\x12\xc0\xfcAi3*;%\x09@\xb9_" + - "\xfaO@/\xfd\x07\xa7\xdf\xaa\xbc%\xed\x00yJ\x92" + - "Y \xe6\xf2\xf8\xc5\xa8l\x1bO\xccw\x8f\xdf\x08\xe8" + - "\xfdi\xf9\xf5C\x7f\xb4\xe7g\xc4,\xc4\xcc\x80\xf3\xcf" + - "\x8d\xdf\x8c\xca\xa7\x9c\xf3\xe3\xf1\xcb\x01\xbd\x8f\x1en\xfa" + - "\x9b\x83\xffr\xf2^\x90\xafF\x08\xcen\xa8\xfb\x0d\x02" + - "*3\xeb~\x04\xe8\xfd\xebu[\xdeY\xfd\xd1\xee\xef" + - "\x0c?7E|/\xd5\x0d\xa1r\xa6N\x02\xd1\xdb\x7f" + - "g\xe3?\xe3\x81Ov\x83|\x0dm\x83\xf4\xf9x\xdd" + - "\x04\x01P\xf9E]\x0e\xd0{\xfd\xda\x13/\xec\xfa\xf1" + - "}\xdf\x07\xf5jD\xf0\xd7\x9f\xaf\xfb\x1f:\x07'\x10" + - "\xc3\x07\x8f\x7f=\xf5\xc3\xd7/\xfb\x01g\xf0\x9e\xf8\xe5" + - "m\xcf\xee\xfa\xf1\x8c\xf7`\x85 a\x0a`\xfe\xec\x09" + - "6\xf1\xde4\x81t\xf1\xf0\xaf\x7f\xb2\xac\xbc\xfb\xd1C" + - "\xfe\xa5\xf9^\x97O\x14\x04Hy;\xba>)\xafx" + - "\xac\xf0XR\x9c\xba\x89\x1f\xd2\xd2i\x13I\x9c\x9b~" + - "sn\xf9\xd2g\xfb\x9e\x0c\x18\xf8E\x9f\x9f\xf8,1" + - "\xbc6\x91\xeeqrc\xf6\x81\xb6?~\xf0\xc9Z\xab" + - "\xa4\xf9\x8d'\x0e\xa1\x92\xae\xa7\x9fX\x7f\x1b\x02zC" + - "\xb7\xfcd\xe5G\x7f\xe1<\x03\xea\\Ly/\xdf\x7f" + - "v\xc3\xec\xa7\xfb^\xe5\xd7\x16\xc9\x8c\x0d\xbf\xa2\xadw" + - "6\xd0\xd9\x0d\x7f?g\xd9\x83\xef,9J['\xcc" + - "\xe2_b\xee\xa4\x16Tn\x9dD\x96\xb9e\x12q\xbf" + - "q\xed\xca\x17_<\xd2\x7f\xb4\xf6\"\xdc\xe2g&-" + - "F\xe5<\xe7~\x97s_\xde\x85o\xfe\xf4\xfa\xd4\xdf" + - "%\x05\xdf\x9fy\x8f\x0e?\x9a!\x86;?;\xfe\x0f" + - "\x0b\xdf?\xf5|\xd2BjV \x0b\xb1,\x09\xde;" + - "\x84\xe57[Z_\x04\xf5\x1aDo\xed\x9e-n\xe7" + - "#;=X\x81\x12\x0a\x00\xf3wf7\xd3f{\xb3" + - "\xe4_\xd3\xce\xb77\x98\xefo\xffi\x8d3\xf2S?" + - "\xce.F\xa5N\xa6\xab\xa5\xe5\x1f\x01~\xf2\xcc}\xbb" + - "\xba\xce.xU\x9d\x82\xa9Z\xa1\x0f\xca\x9bQ9N" + - "\xbc\xf3\x8f\xca\xcd\xa4\xcfH\x835\xec\\\xea\xff\xb8l" + - "-*\x1f_F??\xb8\x8c\xb3/\xbe\xf3{\x0f\xa5" + - "\xcf}\xef\xd5Z\x95R\xe0\xcc\xff\\\xb1Q\x91\x1b\xb9" + - "K7\xfeV\x00\xf4\xa6\x1c\xf9\x93\xbfm/\x9d\xf9\xd9" + - "(A\xa4\xdc:\xf9C\xa5k2\xfdZ8\x99d<" + - ";\xf7\xe8\x9f\xbf\xbb\xf3\x97\xa7\x92\x9erp2\xf7\xd8" + - "\xe3\x93Ia\xf7}}p\xf3\xb2\xab\x86N\xd7\x1a\x88" + - "s\x9e\x9e<\x84\xcay\xbe\xdd\xbb|;\xe1\x9c\xf6\xb5" + - "{\xfe\xed\x1bo&|\xf6\x8e\xa6\xb7\x11R\xde\xb2\x95" + - "w\xae\xad\xbb\xfb\xec\xd9\xe4AK\x9b\xb8\xe9\xb4&:" + - "\xe8\x98\xfc\x90r\xe2\xe0_\xbfC\x07I\xb5\xea\xde\xd6" + - "\xd4\x8b\xca\x9e&\xfa\xb9\xbb\xe9I\x122\x8a\x9d\xd1\x1c" + - "g\xe7\x15-\xa8\xec\xbf\x82\xee\xb5\xf7\x0a\xba\xd7M\xab" + - "\xdb\xd8\xaa\x9bo\x7f\x0f\xe4)\xe20\xa88C\x9c\xef" + - "\x12\xe7\xfcsW\xdc\x87\xca\xf9\xa9\x12\x80\xf7\xdd\xfe\xde" + - "\xd7>\xe88\xf8\xdf\xb5\x9bs\x81NOmA\xe5\xdc" + - "Tn\xaa\xa9\xdc>\xf3\xaf\xff\xcb\xf3{\x1e\xeb\xf8`" + - "\xc4\xees\xa7\xb7\xa3r\xebt\xee\xee\xd3\xbf\xa9\xe8\xd3" + - "\xf9\xe6\xdf^\xb0\xfc\x96Y/}\x98\xd4\x84:\x9dG" + - "/\x9bN\x9a\xe8\xbb\xf9\xbf\xbey\xd5w\xff\xe9\xc3\x1a" + - "\xfbq\xc6{\xa7\xcfAe\x0f\xdfq71\xbf\xbf\xe8" + - "\x07\xa7\xa6d\xa6\xfcn\xb4\x8b\x1e\x9f\xbe\x16\x95\xd7\x88" + - "w\xfe+\xd3y\x1c\xdf\xfe\xf6\xa3\x1bs\xdf\xff\xdd'" + - "$\x97X\x83s\xb7\xcc\xe8Ee\xe9\x0c\xda\xb9k\x06" + - "\xc5\xd2\x92\xc3g\xbe1\xb0\xe7\xe4\xa7\xa3\"\xf7\xc73" + - "\xb6\xa3R7\x93\xfb\xffLB\xab\xbf\x92\xf6\x9d\xbd\xe7" + - "\xb7\x7f\xf6YR\xaa\xf33\xdf\xe6\xd07\x8b\xa4\xda\xf2" + - "\xfe\xde\xce\x07W\x1d\xfe\"\xc90s\xd6\x0b\xc4p=" + - "g\x88\x82q4OSg\xb5\xa3\xa2\xcd\xa2\xf3\xee\x9a" + - "\x95\x83\xb9\x9e[5Mf\xd8\x95T\xf1\x0f\xc3\x9f\xc5" + - "yE\xadbVZ\xda\xaa\xee\x003]\xbd\xa8\xb9\xac" + - "\x9b\xe5\x9c\x8ae:,\x8f\xa8f\xc5\x14@\x0a\x01d" + - "m-\x80\xbaZD\xd5\x10PFl$\xb4\x96u\"" + - "\x0e\x88\xa8\xba\x02\xca\x82\xd0H\x88 \xaf\x9f\x05\xa0\x1a" + - "\"\xaa\x9b\x04D\xb1\x91\xf0N\xae>\x04\xa0n\x12Q" + - "\xdd!\xa0WavY3\x99\x09\x19w\xa1mc=" + - "\x08X\x0f\xe8\xd9\xcc\xb5\x07\xb55\x06dX\x82,\xad" + - "\xdd\xe8b\x03\x08\xd8\x00\xe8\x0dXU\xdbYa\xba\xa8" + - "\x1b\xdd\xac\xcff\x0e\x0e\xe08\x10p\x1c\xe0X\xe2u" + - "X\xa6\xc9\x8an\xa1Z,2\xc7\x01 \xc9\xc6G\x92" + - "\xcd~\x14@\xbdVD\xf5\xe6\x84d7\x91d7\x8a" + - "\xa8\xb6\x0a\xe89\xcc\xde\xc0\xec%\x16\x165W\xb7\xcc" + - "e\x9aXf\xd1\xb5\x8b\x86\xceL\xb7\xc3\x82\x8c\xd9\xa7" + - "\xf7c6\x0e\x05@\xcc\x8e}\xb1\x85\x9bt\xc7\xd5\xcd" + - "\xfe\x1eN\xcf\xe5-C/\x0e\xd2\xed\xea\xb9&\xa7\xb5" + - "\xd0\x1e\xf2\xe5\xbd\x00(\xc8r;@N\xef7-\x9b" + - "y%\xdd)\x92P \x16\xdd\xadk4C3\x8b," + - ":h\xdc\xc8\x83\xfc\x03\x0a\\\x8eyZ\xc2\xdaW\xe6" + - "5[\x13\xcb\x8eZ\x1f\xe9ca/\x80\xba@D5" + - "\x9f\xd0\xc7\xd2\xc5\x00\xea\x12\x11\xd5\xdb\x13\x96^\xd1\x0e" + - "\xa0\xe6ETW\x09\xe8Y\xb6\xde\xaf\x9b\x1d\x0cD;" + - "i0\xc75\xb52\x03\x80Pa[\xad\x0a)\xd1\xc1" + - "l\x8c\xd25\x9aJ\x8f\x14\xa0\x93\x19\x86u\x9be\x1b" + - "\xa5\xe5\xfe9\x16i\x9b\x9b2Z&\x8dbyn\x1c" + - "\x92[/\xb2yU\x87\xf9\xeb\xaa67\xe4\x95\xdd\xcc" + - "\xa9\x1a\xae\x03\xa0\xa6\"\xf1\x1bZ\x00\xd4\xf1\"\xaa\x8d" + - "\x02\xe6l\xce\x80\xd9\x18\xd4k\xaez!]WM\x9b" + - "\xf5\xeb\x8e\xcbl\x9f|e\x8e\x14^v\x92\x07\x92\xff" + - "eET\xa7\x0a\xe8\xf5\xdbZ\x91\xe5\x99\x8d\xbaUZ" + - "\xa6\x99VAdEL\x83\x80\xe9\xb1=i\x91\xa6\x1b" + - "\xac\xe4K7\xaf\xd8\xcc\xff\xa7\xe8\xad\xf7\xe9R\xb5\x1e\x1a:Z5\xf6" + - "z\x9b\x15}\xc8\x08\x96\xe7\x9b}\xa3%\x82\x84t\xd4" + - "*\xa2\xba$\x11$]\x14$\x9d\"\xaa=\x89 Q" + - "\xdb\xe3\xc8\xa9\x01\xb3\xdfSl\x0c\xbbzp\xf1\xe8\xca" + - "\x09?#\x17\xa8\x17Qm\"\x0c\xa6\xaf\xcc\xa5h\xa5" + - "\xd3\xa2\xea\xf6\xc2\xa7u\xd0\xbf\x01\xa2\xe6\x83]\xec\x00" + - "T\x9b\xa2\xc3\xf6\xd2a\x8f\x88\xa8>\x9e\xd0\xcfA\x1b" + - "@= \xa2zX@\x0c\xd4\xf3\xf4!\x00\xf5\xb0\x88" + - "\xeas\x02\xca\xa2\xe0\xfb\xda\xf19\xd4\x0b\x89\xa8\xfe\\" + - "@9%6R\xa9/\xbfFq\xf2s\x11\xd5_\x0b" + - "(\xa7S\x8d\x98\x06\x90O\xaf\x01PO\x89\xa8\xbe\xf5" + - "e\x10T4\xacj\xa9\xcf\xd0\xa0\xd9f\xa5\xae\x05\x11" + - "\xdd\xac\x96\xf36\xdb\xa0\xa3Uu\xda\\\x97\x95\xa5\x8a" + - "\xeb\x84\xd9$\xe3j\xfd\x0eN\x02\xcc\x8b\x88\xd9\xb8>" + - "\x04$b\xb4'\xda\xac\xb4\x92\xd9\x8e.Zf\x94\x10" + - "t\xd3e\xa6\xbbD\x03i\x0d3\"\xea\x18\x80\xd1\x1d" + - "\xb8=9}\x10\xc1V\x0cr\xd8On7\xd5\xf3\x02" + - "%.\x9c\x13{\xde4\xfc\x82\xc8\xa4\xc7\xae\xee\xd8\xf7" + - "\xa6\x09\x9f\x13\x994\xa9\xf6\xc6\x10\x9d\x19p\xdd\x0af" + - "\xe3\xba10\xf6F\xb6\xc6\xb1\x8a\xeb\x18 !]T" + - "\xc4\x04_\x07\x02\xe4\x05\xd1(a6\xee\xfbj\xc2\xfc\xa0E\xd8K\x06\xde'\xa2\xfa\x94" + - "\x80\x98\xf2!\xff\x09\x82\xfc\xa7DT\x8f\x09\x1c\xb0;" + - "\xdb:,\x13\x83K8\x00Q\xf1?\xc04\xdb]\xc3" + - "4t\xbbL\x97\xd9\x1b44BH\xd8\xea\xeaef" + - "U\xdd\x08\"\xca\xda&^-a\xa9\xd3_%i\xae" + - "\x83u `\x1dE\xa4\xc3\xec\x0e\x9b\x95\x90\xac\xa1\x19" + - "yMt\x07.FA\xc3A<3\x8az\xa8\xd6\xda" + - "\"\xa2\xfa\x1d\x82\x12L\x8c\x88\xe4{\xd7\x82\xc0\x91\x84" + - "d^\xdf\x1eW_'\x88~\x92\xe8\xf5\xa9F\xac\x07P^\xc1C" + - "\x00\x85\x93D?\x85\x11\xdeu\x95\x92\xb0K\xee\xa6\xc7" + - "e\x87h9\x91\xc9Y\xd0V\xa2\x9f\x13\xf2V\x86\xfa" + - "J\xcc\xc4\xf3_@\xcc\x00z\x15\xcb2\x96\x0d\x87\xf3" + - "\x0bU>\x81\xbb@\xc62\xbbJQ\xfc\xf9N\xb6\xc4" + - "\x82\xe6\xa2ftU\xe2Z\xc8i\xab\xbaV\xb5\x02\xcd" + - "%\xcde\xa5(!\xdbUs\x91m\x95{\x90\xd9e" + - "\xdd\xd4\x0c\x88\xbe\x8c\xe5s\x99jU/E{\x8fY" + - "\xc0E\xee)\xd4\xbags\xa5\xa5G\xeb\xaf\x19\x01\xcc" + - "\x89\xb1>\x82\xae\xb97\xc4P\x9fI\x86T\xf3\x06\xcd" + - "\xa8\xb2\x8b\xa9\xec\xc6l*\xbas~Sr\xa1\xde3" + - "\x1cX]\xb84_Q\x93F\xfd\xe46b\xde\xd1\x1e" + - "\x0b\x1b\xc9j\x073\x90N!N`\xa1I\xfa\x82\xde" + - "\x12\x9ai\xef\x84sD\x13\xc5\xc09.V\x13\xfd\xcc" + - "\xf5\x7fu\x99}\x16\xe5zI+;_qu7s" + - "2\x17\xa3\xc5xFx\xe1d\xdc\xd9\xd3\x93\x8f\xc7\x0c" + - "\xa2\x8f\xe4\xd7E\xe0\xd5\x86\xdd\x00\x85V\x8a\xce%\x18" + - "\xe9P\xe9\xe2 \xd2I\xe4\x1e\x8cKXE\xe5`\x91" + - "'\xfa*\x8c\x9b\x1c\xe5\x0e\x1e\xe41\x06\xa6\xda|\xf0" + - "b|\xfb\x08\xeb\xe44\xfa\xe0U\xe6\xfb\x1bD\xdf\x94" + - "\x04\xaf*\x0e\x0d\x03;I\xf4\xc1k\x1b\x07\x9d\x1dD" + - "\xdf\xc5\xc1+\xe5\x83\xd7N|\x16\xa0\xb0\x8b\xe8\xfb8" + - "x\xa5}\xf0\xda\x8b/\x0c\x03\xbb\x09\xe3|\xf0z\x82" + - "\xf3?E\xf4c\x1c\xbc\xda}\xf0:\xca\xc1\xee\x08\xd1" + - "O\x10HUm\xa3\xe0\xda\xba\x09\xd8\x1f\xc7F\xb1\xf2" + - "-\xc6*m\x901\xf4\x0d,J,%]3\x16T" + - "5\x03\x9a\x0b\xaeV\\\x17\xd7\xe9\x86\xd3\xa9\x99%\x07" + - "\x07\xb4u\x8c\xd2\x91\x94L\xdc\xae\xe1\xacd\xb6\xde\x07" + - "\x18W\xf6Q!\x93\xc9[Vm}\xc3\x0bDf\xfb" + - "\x08\x17}+k\x9b\xbaJ\x06\xeb\xc0\xb0\x9c\x11\xcd8" + - "\x1d\xea\xf4\xc52M\xf4k\x8c\x1e\xbdyx\xf1P\x09" + - "z\x85\xb0\x08\xe9\xc9\xd5T\x17lS\x85\x15\xdd\x0e\x0b" + - "MW7\xabl\xc4\x06\xc5\x81\xaa\xb9\x8e\x95\x16\xa2Y" + - "\xb4J\xba\xd9\x0f#\x9a\x14\xf1\xcb\xa6;\x89\xaa\x8bG" + - "3&^\xc7\xe4\xd9- p\xe8\xa2\x1aBn\x89[" + - "\xfd\\\x91\xaf\xca\xd9Ls\x12]\xea\x18\xa7\x05\xd3H" + - "?\xc8\xfc\xb6>\x0d\x10=%a8\x8e\x97\x8fn\x06" + - "A~Z\xc2\xf8\x15\x03\xc3G\x0by\xbf\x0d\x82\xbcG" + - "B!z\xe2\xc3\xf0yN\xbe\x7f\x08\x04\xf9^\x09\xc5" + - "\xe8\xd9\x0d\xc3Y\xb7<\xd8\x0e\x82\\\x960\x15=A" + - "b8(\x975\xaa\x93\xee\x900\x1d\xbd\xe7a\xf8\x1a" + - "#/\xdd\x0e\x82\xbcP\xf2\xc2v\x08r\xbe\x18\xad\xe8" + - "\x85\x80\x01\xcd\x1c2Z\xd1\x0b\xe7C\x18\xb6M\x00\xad" + - "\xb85\x80\xe7V\xf4\xc2\x09)d\x8a\x9a\xcbZ\xa9\xd7" + - "\xf4?b\x00\xde\xd0\x8a\xc9\xc9\xa3\xf8e\x0d\xce\xe8\x85" + - "r{\\\xcc\x85\x00\xbcm(\xae\xe5\xa2\xa6r\xe73" + - "\xc9:9\x98\x8d\xec\xdd\x1eLV\x8e%f#G\xa9" + - "x>&\xa2\xfa\x86\x10W\x06\xa1O\x87\xc3:\xb4\xec" + - "\xb0\xcb\x1dcf\x17x~P\xc3\xd6N\xee\xbc\x925" + - "\xc0k\\\xf4\xb7r N\x07\xc9q\xde\xa4\xc48\x0f" + - "\xc3\xfeZ\x1a\x96=\x92\xc3\xbdI\x17h\xd6\x92\xdd\"" + - "Og)\xee\x92\xe1\xe3%\x86\xef\xcc\xb2L\xae\xd5 " + - "yaG\x89a.\x84\x1a\x93]b[\xdd\xcd\x9a\xff" + - "/\xc9z\x14\x07\xf1\xcf\xc9\x90G\xfa\x02E\xfb\xaeM" + - "\xcc\xe9\x0c+\xe8\x083\xcb\x92E\xfd\x18\xba\xf2/\x1c" + - "\x96\xe0\x19Z\\3\x9a\xa3f\xf2a\x11\xd5\x03\x89b" + - "g\xff\xac\xc4\xbc.\xec\xd3\x0e.\x0e\xe6u'\xe2\x97" + - "\x9c\xe7\xc9QO\x88\xa8\x9eL\xb8\xdf+\xc4\xf8\xb2\xef" + - "~a\xbe\x92\x7fA=\xcb\x1b\"\xaao\x06\xadr\xf8" + - "\xa2S\xb5c\xa06\xac\xfe%\xba\xc9\x1c\xaa@k\x06" + - " \xe13\x11\xba\x04\x7fU\x9b0|8Vv-H" + - "\x14\xae\xd1<\x08\x99]\xa0p-\xa1\x13\xcdY\xc6\xd0" + - "W!\x88\x0e?8\x82d\x9fh\xbd\x0f%\xa6R\xa1" + - "\xb6\xd4\x17\x82i\xcf\xea\x84\xb6\xee\xa2\xd6{\x95\x88\xea" + - "\x80\xc0\x11\xc4ZQ)i\xe8\xb2E6[_e\x92" + - "Y\x1c\x8c[Pj\xc2\x8a\xce\x0a\xacPY\xbc\xc8f" + - "\xb9\xf5U\x96d\x08\x9f\x01@\xd2\xad\xd2\x88\xf9\xff(" + - "\xa5\xdfmlM\xc1*\xaec\xee\xb0\xe7\x91\x9a'\xbc" + - "\xee\xf8\x0d z\xc1\xebN\xbe\xe0\x05\xb8\xb3\x9e<\xb0" + - "\"\xa2\xba%\x81;\x83Cq\xff:z\xae\xff\xfd\xa4" + - "\xe7\xaf\xf4\x8aE\x95\xaet1U`\xf4\xc75_q" + - "\xac~\xb1E{\xfc6{\x89\xa3(\x88\xc0\x00\x13\x7f" + - "{A\x87\x08\xc1\xe6\xff\x1b\x00\x00\xff\xff\x99N\xd0U" +const schema_db8274f9144abc7e = "x\xda\xc4Z{\x90\x15ev?\xa7\xfb^\x9a\x81\x19" + + "\xeem{,f\x11\x18\x9d\xd2(\xac\x10\x15It\x12" + + "w^\xc0\xce\xb0\xc2f\xc1\x85\x8a\x1a\xb44" + + "\xd9\x8d\x98\xdd\xc4\xdd\x0d\xa4dc\xd5\xba\"\x95\xc2\x92" + + "2\xbe\xca\x98R;u\xbe~\xce\x9dq\x00\xb3U\xf9" + + "\x07n\x9d>\xdf\xe3\xbc~\xe7\xf1\xcdu\x83\x93Z\x85" + + "\xeb\xd3Wg\x00\xd4C\xe9\x09\x1e\x9b\xfd\xebM\x8f\\" + + "\xf5\x8f\xdb@\x9d\x86\xe8}\xe7\xd8\xe2\xfaO\xddm\xff" + + "\x01iQ\x02\x987$mBe\x87$\x01(\xf7J" + + "\xff\x09\xe8\xa5\xff\xe0\xe4\x9b\xe57\xa5\xed OK2" + + "\x0b\xc4\\\x9a\xb8\x18\x95\xad\x13\x89\xf9\xce\x89\x1b\x00\xbd" + + "?-\xbdv\xe0\x8fv\xff\x9c\x98\x85\x98\x19p\xde\x99" + + "\x89\x9bP\xf9\x94s~" + + "\xb4\xc3\x83\x15(\xa1\x000oGv\x13m\xb6'K" + + "\xfe5\xe3\\{\x9d\xf9\xde\xb6\x97\xaa\x9c\x91\x9f\xfaq" + + "v1*52]--\xff\x18\xf0\x93\xa7\xef\xd9\xd9" + + "uz\xc1+\xea4LU\x0b\xbd_\xde\x84\xcaQ\xe2" + + "\x9dwXn$}F\x1a\xacb\xe7R\xff\xee\x92A" + + "T>\xbe\x84~\xbe\x7f\x09g_|\xc7\xf7\x1fH\x9f" + + "\xf9\xfe+\xd5*\xa5\xc0\x99\xf7\xb9b\xa3\"\xd7s\x97" + + "\xae\xff\xad\x00\xe8M;\xf4'\x7f\xdb^<\xf5\xf31" + + "\x82H\xb9e\xea\x07J\xd7T\xfa\xb5p*\xc9xz" + + "\xce\xe1??\xbb\xe3W'\x92\x9e\xb2\x7f*\xf7\xd8\xa3" + + "SIa\xf7|}h\xd3\xb2\xab\x86OV\x1b\x88s" + + "\x9e\x9c:\x8c\xca9\xbe\xddY\xbe\x9dpF\xfb\xda]" + + "\xff\xfe\x8d7\x12>{{\xc3[\x08)o\xd9\xca;" + + "\x06k\xee<}:y\xd0\xd2\x06n:\xad\x81\x0e:" + + "\"?\xa0\x1c\xdb\xff\xd7o\xd3AR\xb5\xba\xb76\xf4" + + "\xa2\xb2\xbb\x81~\xeejx\x82\x84\x8cbg,\xc7\xd9" + + "qY3*\xfb.\xa3{\xed\xb9\x8c\xee5\x7fu\x1b" + + "[u\xd3m\xef\x80gf;*\xb7\xcc\xe4\xee>\xf3\x9b\x8a>\x93o" + + "\xfe\x9d\x05\xcbonz\xf9\x83\xa4&\xd4\x99:s\x10\x95W\x89w\xde" + + "\xcff\xdeO\x17\xbd\xed\xad\x877\xb4\xfc\xe0\xa3OH" + + ".\xb1\x0a\xe7\x86.\xefEe\xc7\xe5\x1c\x8b/\xa7X" + + "Zr\xf0\xd47\x06v\x1f\xfftL\xe4\x9e\x7f\xc56" + + "T\xba\xae\xe0\x8et\x05\xa1\xd5_I{O\xdf\xf5\xdb" + + "?\xfb,)\xd5\x9c\xa6\xb7H\xaa\xb6&\x92j\xf3{" + + "{:\xef_u\xf0\x8b$\x83\xd6\xf4<1\xac\xe3\x0c" + + "Q0\x8e\xe5i\xbb\x9a\xdaQ\xd9\xdfD\xe7\xedkj" + + "\x819\x9e[1Mf\xd8\xe5T\xe1\x0f\xc3\x9f\x85\xb9" + + "\x05\xadl\x96\x9b\xdb*\xee\x003]\xbd\xa0\xb9\xac\x9b" + + "\xb58e\xcbtX\x0eQ\xcd\x8a)\x80\x14\x02\xc8\xda" + + " \x80\xbaZD\xd5\x10PF\xac'\xb4\x96u\"\x0e" + + "\x88\xa8\xba\x02\xca\x82PO\x88 \xafk\x02P\x0d\x11" + + "\xd5\x8d\x02\xa2XOx'W\x1e\x00P7\x8a\xa8n" + + "\x17\xd0+3\xbb\xa4\x99\xcc\x84\x8c\xbb\xd0\xb6\xb1\x16\x04" + + "\xac\x05\xf4l\xe6\xdaC\xda\x1a\x032,A\x96\x067" + + "\xb8X\x07\x02\xd6\x01z\x03V\xc5vV\x98.\xeaF" + + "7\xeb\xb3\x99\x83\x038\x01\x04\x9c\x008\x9ex\x1d\x96" + + "i\xb2\x82\x9b\xaf\x14\x0a\xccq\x00H\xb2\x89\x91d\xb3" + + "\x1e\x06P\xaf\x15Q\xbd)!\xd9|\x92\xecF\x11\xd5" + + "V\x01=\x87\xd9\xeb\x99\xbd\xc4\xc2\x82\xe6\xea\x96\xb9L" + + "\x13K,\xbav\xc1\xd0\x99\xe9vX\x901\xfb\xf4~" + + "\xcc\xc6\xa1\x00\x88\xd9\xf1/\xb6p\xa3\xee\xb8\xba\xd9\xdf" + + "\xc3\xe9-9\xcb\xd0\x0bCt\xbbZ\xae\xc9\x19\xcd\xb4" + + "\x87|i/\x00\x0a\xb2\xdc\x0e\xd0\xa2\xf7\x9b\x96\xcd\xbc" + + "\xa2\xee\x14H(\x10\x0b\xee\x965\x9a\xa1\x99\x05\x16\x1d" + + "4a\xf4A\xfe\x01y.\xc7\\-a\xed+s\x9a" + + "\xad\x89%G\xad\x8d\xf4\xb1\xb0\x17@] \xa2\x9aK" + + "\xe8c\xe9b\x00u\x89\x88\xeam\x09K\xafh\x07P" + + "s\"\xaa\xab\x04\xf4,[\xef\xd7\xcd\x0e\x06\xa2\x9d4" + + "\x98\xe3\x9aZ\x89\x01@\xa8\xb0-V\x99\x94\xe8`6" + + "F\xe9*M\xa5G\x0b\xd0\xc9\x0c\xc3\xba\xd5\xb2\x8d\xe2" + + "r\xff\x1c\x8b\xb4\xcdM\x19-\x93\xc6\xb0<7\x0e\xc9" + + "\xad\x17\xd8\xdc\x8a\xc3\xfcu\x15\x9b\x1b\xf2\xcan\xe6T" + + "\x0c\xd7\x01PS\x91\xf8u\xcd\x00\xeaD\x11\xd5z\x01" + + "[l\xce\x80\xd9\x18\xd4\xab\xaez>]WL\x9b\xf5" + + "\xeb\x8e\xcbl\x9f|e\x0b)\xbc\xe4$\x0f$\xff\xcb" + + "\x8a\xa8N\x17\xd0\xeb\xb7\xb5\x02\xcb1\x1bu\xab\xb8L" + + "3\xad\xbc\xc8\x0a\x98\x06\x01\xd3\xe3{\xd2\"M7X" + + "\xd1\x97nn\xa1\x91\xffO\xd1[\xeby~\xf8\xf6\xc6" + + "\xe1[\x87_xA\xfcn\x8a\xe3\xb7N\xf8\xdc\x1b\x1d" + + "\xc0u\xe2g^\x10\xc2\x14\x11\xae\x88\xea]\x14\x11\x95" + + "2\xe9\xd4\x01\xd1\xb21\x1b\xa3d\xa0\x1dV\xec'M" + + "\x9b\xd0\xc2\x0a\xa4h\xcc\x86\xd9\xdeg\x90\x8a\xd6\x00f" + + "\xe3R&Xf\xb3\xf5\xccvX\x0e2\xb6\xb5q\x08" + + "\xb3q\xd6\xaf\xd2\xfa\x94\x8b\xd5zh\xe8h\xd5\xf8\xeb" + + "mV\xf0!#X\x9ek\xf4\x8d\x96\x08\x12\xd2Q\xab" + + "\x88\xea\x92D\x90tQ\x90t\x8a\xa8\xf6$\x82Dm" + + "\x8f#\xa7\x0a\xcc~O\xb11\xe2\xea\xc1\xc5\xa3+'" + + "\xfc\x8c\\\xa0VD\xb5\x810\x98\xbe2\x97\xa2\x95N" + + "\x8b\xaa\xdb\xf3\x9f\xd6A\xff\x06\x88\x9a\x0bv\xb1\x03P" + + "m\x88\x0e\xdbC\x87=$\xa2\xfaXB?\xfbm\x00" + + "\xf5\x11\x11\xd5\x83\x02b\xa0\x9e\xa7\x0e\x00\xa8\x07ET" + + "\x9f\x15P\x16\x05\xdf\xd7\x8e\xce\xa6^HD\xf5\x17\x02" + + "\xca)\xb1\x9eJ}\xf9U\x8a\x93_\x88\xa8\xbe.\xa0" + + "\x9cN\xd5c\x1a@>\xb9\x06@=!\xa2\xfa\xe6\x97" + + "AP\xc1\xb0*\xc5>C\x83F\x9b\x15\xbb\x16Dt" + + "\xb3R\xca\xd9l\xbd\x8eV\xc5is]V\x92\xca\xae" + + "\x13f\x93\x8c\xab\xf5;8\x050'\"f\xe3\xfa\x10" + + "\x90\x88\xd1\x9eh\xb3\xe2Jf;\xbah\x99QB\xd0" + + "M\x97\x99\xee\x12\x0d\xa45\xcc\x88\xa8\xe3\x00Fw\xe0" + + "\xf6\xe4\xf4A\x04[1\xc8a?\xb9\xddt\xcf\x0b\x94" + + "\xb8pv\xecy3\xf0\x0b\"\x93\x1e\xbb\xbac\xdf\x9b" + + "!|Nd\xd2\xa4\xda\x1bCtf\xc0u\xcb\x98\x8d" + + "\xeb\xc6\xc0\xd8\x1b\xd8\x1a\xc7*\xace\x80\x84tQ\x11" + + "\x13|\x1d\x08\x90\x17D\xa3\x88\xd9\xb8\xef\xab\xf2\x14\xf1" + + "\xcb\xd2n\x0b%y\xcb\xe6Y-\x0e\x9f\x1bb!\xa2" + + "\xe8\xe9MFOk\x10=k\xe2\xfb7\x16\xb4\x8a\xc3" + + "F\xd6\x0bm}.\x88\xcc\x8e \xd2\x19\xb0*F\xb1" + + "\x9b\x81\xe4\xdaC\x88 \x8e\x0f\x9c\x0b\xac\xce\x84\xe2" + + "}7\x1e;\x17F\xa9\xb07\x99\x0a\x03\xf5\xaf \xf5" + + "\xf7\x88\xa8\x96\x05\xf4\x0c\x82\x1e\xb3\xd3\x02\xd1q\xa3\xeb" + + "\xfa\xc4\x9c\xc5\x9dS\x02\x01%@\xafRv\\\x9bi" + + "%\xc0\xc8\xdb\x88\x7f\xcaEd\x98*\xa4\xcbi\x99j" + + "\xa8\xfa\x7f\xcf\xe7\x17\x9f\x98\xfd$9\"-\x1fHd" + + "\xc9B\xb0\x1a\xf9\xf2\x0e\xcb\x94.\xba\xf4\x0a\x10\xccO" + + "\x0cs\x83DOUa\x981g\x11\xc2_)\xa2z" + + "]2c\xce!\x15]#\xa2z\xa3\x80\x12\xb3)\xf9" + + "E\xed\xbb\x7f\xe8\x16\xc7/31\x1b\xcff\xce\x7f\x9d" + + "D\x05\xae[\xe6(7\x1c3\xdb\xdc\x90\xb0kh\xc2" + + "\xa5k\x12\xd9f-\x1b\x0a\xad\xd4\xc8J\x9a\x1e\xa3Q" + + "`\xdc6\x90\xbe\x15\xf3\x8c[\xa9\x06\x19\xdd\xcf\xe7-" + + "\xbe\xb5\xe8\x92\xf5\xd1%\xef\x1c\x06P\xef\x12Q\xbd/" + + "q\xc9{\xa9\xf0\xbfOD\xf5\xa1\xc4%w\x93\x12w" + + "\x8a\xa8\xee%\xcc\x0fZ\x84=d\xe0\xbd\"\xaaO\x0a" + + "\x88)\x1f\xf2\x1f'\xc8\x7fRD\xf5\x88\xc0\x01\xbb\xb3" + + "\xad\xc321\xb8\x84\x03\x10\x15\xff\x03L\xb3\xdd5L" + + "C\xb7\xcbt\x99\xbd^C#\x84\x84-\xae^bV" + + "\xc5\x8d \xa2\xa4m\xe4\xd5\x12\x16;\xfdU\x92\xe6:" + + "X\x03\x02\xd6PD:\xcc\xee\xb0Y\x11\xc9\x1a\x9a\x91" + + "\xd3Dw\xe0B\x144\x12\xc43c\xa8\x87j\xad\xcd" + + "\"\xaa\xdf%(\xc1\xc4\x88H\xbe{\x10\x04\x8e$$" + + "\xf3\xba\xf6\xb8\xfa\xe2\x091]\xd5?\xf1\x848\x01@" + + "\xdeJ\xda\xd9.\xa2\xbaS\x08\xaf\xd6iA\x8b\x1f\xa1" + + "\xd5\xa6\x0e\xfa\x93-\x84\x9a:\x8b\xe5\x0d\xea\x05\x1d-" + + "\xb3\x87+\x0acM\x15\xacR\xd9&W\xd6-S\xad" + + "h\x86.\xbaC\xd1\xc2quA\x90\xe4\x87\xf2\xf2r" + + "#7\x16)\xe3\xc6P\x19\xca\xb7q1@~\x15\x8a" + + "\x98\x1f\xc0\xd8]\x14\x86\xed\x00\xf9\xd5D70\xf6\x18" + + "E\xc7i\x00\xf9\"\xd1\xcb\x18\xb5\x95J\x09\x9f\x06\xc8" + + "\x97\x89\xbc\x19\xe3RA\x19\xe2\xdbo$\xfav\x8c\xab" + + "\x05e+\xce\x06\xc8o&\xfaCD\x9f pM*" + + "\xbbq\x10 \xff \xd1\x1f!\xba\x94\xaeG\xde8\xa3" + + "\x0d\x90\xdfK\xf4'\x89>\xb1\xa1\x1e'\x02(\x8fs" + + "\xfacD?D\xf4\x9a\xaf\xd5c\x0d\x80\xf2#\xdc\x06" + + "\x90?H\xf4g\x89>\x09\xebq\x12\x80r\x14\x1f\x06" + + "\xc8?K\xf4\x9f\x12}\xf2\x84z\x9c\x0c\xa0\xbc\xcc\xef" + + "s\x8c\xe8\xc7\x89^\x9b\xaa\xc7Z\x00\xe5gx\x00 " + + "\x7f\x9c\xe8'0\xc2\xbb\xaeb\x12v\xc9\xdd\xf4\xb8\xec" + + "\x10-'29\x0b\xdaJ\xf4sB\xce\xcaP_\x89" + + "\x99x\xfe\x0b\x88\x19@\xaflY\xc6\xb2\x91p~\xbe" + + "\xca'p\x17\xc8XfW1\x8a?\xdf\xc9\x96X\xd0" + + "X\xd0\x8c\xaer\\\x0b9m\x15\xd7\xaa\x94\xa1\xb1\xa8" + + "\xb9\xac\x18%d\xbbb.\xb2\xadR\x0f2\xbb\xa4\x9b" + + "\x9a\x01\xd1\x97\xf1|.S\xa9\xe8\xc5h\xefq\x0b\xb8" + + "\xc8=\x85j\xf7l,7\xf7h\xfdU#\x80\xd91" + + "\xd6G\xd05\xe7\x86\x18\xea3\xc9\x90j\\\xaf\x19\x15" + + "v!\x95\xdd\xb8MEw\x8b\xdf\x94\x9c\xaf\xf7\x0c\x07" + + "V\xe7/\xcdWT\xa5Q?\xb9\x8d\x9aw\xb4\xc7\xc2" + + "F\xb2\xda\xc1\x0c\xa4S\x88\x13Xh\x92\xbe\xa0\xb7\x84" + + "F\xda;\xe1\x1c\xd1D1p\x8e\x0b\xd5D?s\xfd" + + "_]f\x9fE\xb9^\xd2J\xceW\\\xdd\xcd\x9c\xcc" + + "\x85h1\x9e\x11\x9e?\x19w\xf6\xf4\xe4\xe21\x83\xe8" + + "#\xf9u\x11x\xb5a7@\xbe\x95\xa2s\x09F:" + + "T\xba8\x88t\x12\xb9\x07\xe3\x12VQ9X\xe4\x88" + + "\xbe\x0a\xe3&G\xb9\x9d\x07y\x8c\x81\xa96\x1f\xbc\x18" + + "\xdf>\xc2:9\x8d>x\x95\xf8\xfe\x06\xd17&\xc1" + + "\xab\x82\xc3#\xc0N\x12}\xf0\xda\xcaAg;\xd1w" + + "r\xf0J\xf9\xe0\xb5\x03\x9f\x01\xc8\xef$\xfa^\x0e^" + + "i\x1f\xbc\xf6\xe0\xf3#\xc0n\xd2\x04\x1f\xbc\x1e\xe7\xfc" + + "O\x12\xfd\x08\x07\xafv\x1f\xbc\x0es\xb0;D\xf4c" + + "\x04R\x15\xdb\xc8\xbb\xb6n\x02\xf6\xc7\xb1Q(\x7f\x8b" + + "\xb1r\x1bd\x0c}=\x8b\x12KQ\xd7\x8c\x05\x15\xcd" + + "\x80\xc6\xbc\xab\x15\xd6\xc6u\xba\xe1tjf\xd1\xc1\x01" + + "m-\xa3t$%\x13\xb7k8+\x99\xad\xf7\x01\xc6" + + "\x95}T\xc8dr\x96U]\xdf\xf0\x02\x91\xd9>\xc2" + + "E\xdfJ\xda\xc6\xae\xa2\xc1:0,gD3N\x87" + + ":}\xb1L\x13\xfd\x1a\xa3Go\x1cY<\x94\x83^" + + "!,BzZ\xaa\xaa\x0b\xb6\xb1\xcc\x0an\x87\x85\xa6" + + "\xab\x9b\x156j\x83\xc2@\xc5\\\xcb\x8a\x0b\xd1,X" + + "E\xdd\xec\x87QM\x8a\xf8e\xd3\x9dD\xd5\xc5\xa3\x19" + + "\x13\xafc\xf2\xacf\x108tQ\x0d!7\xc7\xad~" + + "K\x81\xafj\xb1\x99\xe6$\xba\xd4qN\x0b\xa6\x91~" + + "\x90\xf9m}\x1a zJ\xc2p\x1c/\x1f\xde\x04\x82" + + "\xfc\x94\x84\xf1+\x06\x86\x8f\x16\xf2>\x1b\x04y\xb7\x84" + + "B\xf4\xc4\x87\xe1\xf3\x9c|\xef0\x08\xf2\xdd\x12\x8a\xd1" + + "\xb3\x1b\x86\xb3ny\xa8\x1d\x04\xb9$a*z\x82\xc4" + + "pP.kT'\xdd.a:z\xcf\xc3\xf05F" + + "^\xba\x0d\x04y\xa1\xe4\x85\xed\x10\xb4\xf8b\xb4\xa2\x17" + + "\x02\x064r\xc8hE/\x9c\x0fa\xd86\x01\xb4\xe2" + + "\x96\x00\x9e[\xd1\x0b'\xa4\x90)h.k\xa5^\xd3" + + "\xff\x88\x01xC+&'\x8f\xe2\x9758c\x17\xca" + + "\xedq1\x17\x02\xf0\xd6\xe1\xb8\x96\x8b\x9a\xca\x1dO'" + + "\xeb\xe4`6\xb2g[0Y9\x92\x98\x8d\x1c\xa6\xe2" + + "\xf9\x88\x88\xea/\x85\xb82\x08}:\x1c\xd6\xa1e\x87" + + "]\xee83\xbb\xc0\xf3\x83\x1a\xb6zr\xe7\x15\xad\x01" + + "^\xe3\xa2\xbf\x95\x03q:H\x8e\xf3\xa6$\xc6y\x18" + + "\xf6\xd7\xd2\x88\xec\x91\x1c\xeeM9O\xb3\x96\xec\x16y" + + ":Kq\x97\x0c\x1f/1|g\x96er\xad:\xc9" + + "\x0b;J\x0cs!T\x99\xec\"\xdb\xean\xd6\xf8\x7f" + + "I\xd6c8\x88\x7fN\x86<\xd2\x17(\xdaw01" + + "\xa73\xac\xa0#\xcc,K\x16\xf5\xe3\xe8\xca\xbfpX" + + "\x82gh1\xed?=\xda\xffhS0\\;\x96(" + + "v\x9ek\x0a\x1c\xe8\xa5D\x9f\xf6\xc2b\x00\xf5\x98?" + + "q\x0b_rN\x92\xa3\xbe.\xa2\xfav\xc2\xfd~G" + + "\x8co\x8a\xa8\xbe\x1b\xe7+\xf9,\xf5,\xef\x8a\xa8\xfe" + + "7%\xab\x94\xdf\xb3|L\xfd\xe9G\"vc\xd0?" + + "\x87\xcf<\x15;Fo\xc3\xea_\xa2\x9b\xcc\xa1\xb2\xb4" + + "j*\x12\xbe\x1d\xa1K\x98X\xb1\x09\xd8G\x02h\xd7" + + "\x82D5\x1b\x0d\x89\x90\xd9y\x8a\xe1\":\xd1\xf0\x85" + + "\xadg\xa6\xbb@\xef\x07\x899\xf1\x88c\x1c\xd5\xe6\x83" + + "@\xf2\xe3(\xa8\x0b\x12]\xfa\x81\xc4\x00+T\xac\xfa" + + "|0\x18Z\x9dP\xec\xb7I\x0b\xabDT\x07\x04\x0e" + + "6\xd6\x8arQC\x97-\xb2\xd9\xba\x0a\x93\xcc\xc2P" + + "\xdc\xadR\xbfVpV`\x99*\xe8E6kYW" + + "aI\x86\xf0\xc5\x00$\xdd*\x8ez*\x18\xa3J\xbc" + + "\x95\xad\xc9[\x85\xb5\xcc\x1d\xf1\x92R\xf5\xda\xd7\x1d?" + + "\x17D\x8f}\xdd\xc9\xc7\xbe\x00\xa2\xd6\x91\xb3\x96ET" + + "7' jh8nu\xc7.\x0b~?\x99\xfc+" + + "=xQQ,]H\xc1\x18\xfd\x1d\xceW\x9c\xc0_" + + "h}\x1f?\xe3^\xe4\xd4\x0a\"\xdc\xc0\xc4\x9fi\xd0" + + "!B\xb0\xf9\xff\x06\x00\x00\xff\xff\"\xcf\xdc\xc0" func init() { schemas.Register(schema_db8274f9144abc7e, From 43babbc2f9a56116fb695159d33737bfc06900a3 Mon Sep 17 00:00:00 2001 From: Ashcon Partovi Date: Fri, 22 Nov 2019 10:07:14 -0800 Subject: [PATCH 15/21] Fix "happy eyeballs" not being disabled since Golang 1.12 upgrade * The Dialer.DualStack setting is now ignored and deprecated; RFC 6555 Fast Fallback ("Happy Eyeballs") is now enabled by default. To disable, set Dialer.FallbackDelay to a negative value. --- cmd/cloudflared/tunnel/configuration.go | 9 ++++++--- connection/manager.go | 2 +- origin/tunnel.go | 2 +- tunnelrpc/pogs/config.go | 10 ++++++---- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 9fddcf7a..8986c9f9 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -203,11 +203,14 @@ func prepareTunnelConfig( TLSClientConfig: &tls.Config{RootCAs: originCertPool, InsecureSkipVerify: c.IsSet("no-tls-verify")}, } - dialContext := (&net.Dialer{ + dialer := &net.Dialer{ Timeout: c.Duration("proxy-connect-timeout"), KeepAlive: c.Duration("proxy-tcp-keepalive"), - DualStack: !c.Bool("proxy-no-happy-eyeballs"), - }).DialContext + } + if c.Bool("proxy-no-happy-eyeballs") { + dialer.FallbackDelay = -1 // As of Golang 1.12, a negative delay disables "happy eyeballs" + } + dialContext := dialer.DialContext if c.IsSet("unix-socket") { unixSocket, err := config.ValidateUnixSocket(c) diff --git a/connection/manager.go b/connection/manager.go index 7d9727e1..bb8d5e23 100644 --- a/connection/manager.go +++ b/connection/manager.go @@ -202,7 +202,7 @@ func (em *EdgeManager) dialEdge(ctx context.Context, edgeIP *net.TCPAddr) (*tls. dialCtx, dialCancel := context.WithTimeout(ctx, timeout) defer dialCancel() - dialer := net.Dialer{DualStack: true} + dialer := net.Dialer{} edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeIP.String()) if err != nil { return nil, dialError{cause: errors.Wrap(err, "DialContext error")} diff --git a/origin/tunnel.go b/origin/tunnel.go index 86a50a16..81605025 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -465,7 +465,7 @@ type TunnelHandler struct { noChunkedEncoding bool } -var dialer = net.Dialer{DualStack: true} +var dialer = net.Dialer{} // NewTunnelHandler returns a TunnelHandler, origin LAN IP and error func NewTunnelHandler(ctx context.Context, diff --git a/tunnelrpc/pogs/config.go b/tunnelrpc/pogs/config.go index ac204484..a15caca3 100644 --- a/tunnelrpc/pogs/config.go +++ b/tunnelrpc/pogs/config.go @@ -197,11 +197,14 @@ func (hc *HTTPOriginConfig) Service() (originservice.OriginService, error) { return nil, err } - dialContext := (&net.Dialer{ + dialer := &net.Dialer{ Timeout: hc.ProxyConnectionTimeout, KeepAlive: hc.TCPKeepAlive, - DualStack: hc.DialDualStack, - }).DialContext + } + if !hc.DialDualStack { + dialer.FallbackDelay = -1 + } + dialContext := dialer.DialContext transport := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: dialContext, @@ -270,7 +273,6 @@ func (*HelloWorldOriginConfig) Service() (originservice.OriginService, error) { DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, - DualStack: true, }).DialContext, TLSClientConfig: &tls.Config{ RootCAs: rootCAs, From 379cb16efe8fe098e027eab93397feb278da82bc Mon Sep 17 00:00:00 2001 From: Adam Chalmers Date: Mon, 25 Nov 2019 11:57:06 -0600 Subject: [PATCH 16/21] TUN-2591: ReconnectTunnel now sends EventDigest --- tunnelrpc/pogs/reconnect_tunnel.go | 13 +- tunnelrpc/pogs/tunnelrpc.go | 2 +- tunnelrpc/tunnelrpc.capnp | 2 +- tunnelrpc/tunnelrpc.capnp.go | 532 +++++++++++++++-------------- 4 files changed, 287 insertions(+), 262 deletions(-) diff --git a/tunnelrpc/pogs/reconnect_tunnel.go b/tunnelrpc/pogs/reconnect_tunnel.go index aef2f66f..d3f73528 100644 --- a/tunnelrpc/pogs/reconnect_tunnel.go +++ b/tunnelrpc/pogs/reconnect_tunnel.go @@ -12,6 +12,10 @@ func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnec if err != nil { return err } + eventDigest, err := p.Params.EventDigest() + if err != nil { + return err + } hostname, err := p.Params.Hostname() if err != nil { return err @@ -25,7 +29,7 @@ func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnec return err } server.Ack(p.Options) - registration, err := i.impl.ReconnectTunnel(p.Ctx, jwt, hostname, pogsOptions) + registration, err := i.impl.ReconnectTunnel(p.Ctx, jwt, eventDigest, hostname, pogsOptions) if err != nil { return err } @@ -38,7 +42,8 @@ func (i TunnelServer_PogsImpl) ReconnectTunnel(p tunnelrpc.TunnelServer_reconnec func (c TunnelServer_PogsClient) ReconnectTunnel( ctx context.Context, - jwt []byte, + jwt, + eventDigest []byte, hostname string, options *RegistrationOptions, ) (*TunnelRegistration, error) { @@ -48,6 +53,10 @@ func (c TunnelServer_PogsClient) ReconnectTunnel( if err != nil { return err } + err = p.SetEventDigest(eventDigest) + if err != nil { + return err + } err = p.SetHostname(hostname) if err != nil { return err diff --git a/tunnelrpc/pogs/tunnelrpc.go b/tunnelrpc/pogs/tunnelrpc.go index d1c771f1..bbf0001a 100644 --- a/tunnelrpc/pogs/tunnelrpc.go +++ b/tunnelrpc/pogs/tunnelrpc.go @@ -433,7 +433,7 @@ type TunnelServer interface { UnregisterTunnel(ctx context.Context, gracePeriodNanoSec int64) error Connect(ctx context.Context, parameters *ConnectParameters) (ConnectResult, error) Authenticate(ctx context.Context, originCert []byte, hostname string, options *RegistrationOptions) (*AuthenticateResponse, error) - ReconnectTunnel(ctx context.Context, jwt []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) + ReconnectTunnel(ctx context.Context, jwt, eventDigest []byte, hostname string, options *RegistrationOptions) (*TunnelRegistration, error) } func TunnelServer_ServerToClient(s TunnelServer) tunnelrpc.TunnelServer { diff --git a/tunnelrpc/tunnelrpc.capnp b/tunnelrpc/tunnelrpc.capnp index 4f4948a1..779f1b36 100644 --- a/tunnelrpc/tunnelrpc.capnp +++ b/tunnelrpc/tunnelrpc.capnp @@ -293,7 +293,7 @@ interface TunnelServer { unregisterTunnel @2 (gracePeriodNanoSec :Int64) -> (); connect @3 (parameters :CapnpConnectParameters) -> (result :ConnectResult); authenticate @4 (originCert :Data, hostname :Text, options :RegistrationOptions) -> (result :AuthenticateResponse); - reconnectTunnel @5 (jwt :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); + reconnectTunnel @5 (jwt :Data, eventDigest :Data, hostname :Text, options :RegistrationOptions) -> (result :TunnelRegistration); } interface ClientService { diff --git a/tunnelrpc/tunnelrpc.capnp.go b/tunnelrpc/tunnelrpc.capnp.go index 4e3ce630..1b29410e 100644 --- a/tunnelrpc/tunnelrpc.capnp.go +++ b/tunnelrpc/tunnelrpc.capnp.go @@ -2890,7 +2890,7 @@ func (c TunnelServer) ReconnectTunnel(ctx context.Context, params func(TunnelSer Options: capnp.NewCallOptions(opts), } if params != nil { - call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 3} + call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 4} call.ParamsFunc = func(s capnp.Struct) error { return params(TunnelServer_reconnectTunnel_Params{Struct: s}) } } return TunnelServer_reconnectTunnel_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))} @@ -3888,12 +3888,12 @@ type TunnelServer_reconnectTunnel_Params struct{ capnp.Struct } const TunnelServer_reconnectTunnel_Params_TypeID = 0xa353a3556df74984 func NewTunnelServer_reconnectTunnel_Params(s *capnp.Segment) (TunnelServer_reconnectTunnel_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}) return TunnelServer_reconnectTunnel_Params{st}, err } func NewRootTunnelServer_reconnectTunnel_Params(s *capnp.Segment) (TunnelServer_reconnectTunnel_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}) + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}) return TunnelServer_reconnectTunnel_Params{st}, err } @@ -3921,37 +3921,51 @@ func (s TunnelServer_reconnectTunnel_Params) SetJwt(v []byte) error { return s.Struct.SetData(0, v) } -func (s TunnelServer_reconnectTunnel_Params) Hostname() (string, error) { +func (s TunnelServer_reconnectTunnel_Params) EventDigest() ([]byte, error) { p, err := s.Struct.Ptr(1) + return []byte(p.Data()), err +} + +func (s TunnelServer_reconnectTunnel_Params) HasEventDigest() bool { + p, err := s.Struct.Ptr(1) + return p.IsValid() || err != nil +} + +func (s TunnelServer_reconnectTunnel_Params) SetEventDigest(v []byte) error { + return s.Struct.SetData(1, v) +} + +func (s TunnelServer_reconnectTunnel_Params) Hostname() (string, error) { + p, err := s.Struct.Ptr(2) return p.Text(), err } func (s TunnelServer_reconnectTunnel_Params) HasHostname() bool { - p, err := s.Struct.Ptr(1) + p, err := s.Struct.Ptr(2) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) HostnameBytes() ([]byte, error) { - p, err := s.Struct.Ptr(1) + p, err := s.Struct.Ptr(2) return p.TextBytes(), err } func (s TunnelServer_reconnectTunnel_Params) SetHostname(v string) error { - return s.Struct.SetText(1, v) + return s.Struct.SetText(2, v) } func (s TunnelServer_reconnectTunnel_Params) Options() (RegistrationOptions, error) { - p, err := s.Struct.Ptr(2) + p, err := s.Struct.Ptr(3) return RegistrationOptions{Struct: p.Struct()}, err } func (s TunnelServer_reconnectTunnel_Params) HasOptions() bool { - p, err := s.Struct.Ptr(2) + p, err := s.Struct.Ptr(3) return p.IsValid() || err != nil } func (s TunnelServer_reconnectTunnel_Params) SetOptions(v RegistrationOptions) error { - return s.Struct.SetPtr(2, v.Struct.ToPtr()) + return s.Struct.SetPtr(3, v.Struct.ToPtr()) } // NewOptions sets the options field to a newly @@ -3961,7 +3975,7 @@ func (s TunnelServer_reconnectTunnel_Params) NewOptions() (RegistrationOptions, if err != nil { return RegistrationOptions{}, err } - err = s.Struct.SetPtr(2, ss.Struct.ToPtr()) + err = s.Struct.SetPtr(3, ss.Struct.ToPtr()) return ss, err } @@ -3970,7 +3984,7 @@ type TunnelServer_reconnectTunnel_Params_List struct{ capnp.List } // NewTunnelServer_reconnectTunnel_Params creates a new list of TunnelServer_reconnectTunnel_Params. func NewTunnelServer_reconnectTunnel_Params_List(s *capnp.Segment, sz int32) (TunnelServer_reconnectTunnel_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 3}, sz) + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 4}, sz) return TunnelServer_reconnectTunnel_Params_List{l}, err } @@ -3996,7 +4010,7 @@ func (p TunnelServer_reconnectTunnel_Params_Promise) Struct() (TunnelServer_reco } func (p TunnelServer_reconnectTunnel_Params_Promise) Options() RegistrationOptions_Promise { - return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(2)} + return RegistrationOptions_Promise{Pipeline: p.Pipeline.GetPipeline(3)} } type TunnelServer_reconnectTunnel_Results struct{ capnp.Struct } @@ -4316,251 +4330,253 @@ func (p ClientService_useConfiguration_Results_Promise) Result() UseConfiguratio return UseConfigurationResult_Promise{Pipeline: p.Pipeline.GetPipeline(0)} } -const schema_db8274f9144abc7e = "x\xda\xc4Z{\x90\x15ev?\xa7\xfb^\x9a\x81\x19" + - "\xeem{,f\x11\x18\x9d\xd2(\xac\x10\x15It\x12" + - "w^\xc0\xce\xb0\xc2f\xc1\x85\x8a\x1a\xb44" + - "\xd9\x8d\x98\xdd\xc4\xdd\x0d\xa4dc\xd5\xba\"\x95\xc2\x92" + - "2\xbe\xca\x98R;u\xbe~\xce\x9dq\x00\xb3U\xf9" + - "\x07n\x9d>\xdf\xe3\xbc~\xe7\xf1\xcdu\x83\x93Z\x85" + - "\xeb\xd3Wg\x00\xd4C\xe9\x09\x1e\x9b\xfd\xebM\x8f\\" + - "\xf5\x8f\xdb@\x9d\x86\xe8}\xe7\xd8\xe2\xfaO\xddm\xff" + - "\x01iQ\x02\x987$mBe\x87$\x01(\xf7J" + - "\xff\x09\xe8\xa5\xff\xe0\xe4\x9b\xe57\xa5\xed OK2" + - "\x0b\xc4\\\x9a\xb8\x18\x95\xad\x13\x89\xf9\xce\x89\x1b\x00\xbd" + - "?-\xbdv\xe0\x8fv\xff\x9c\x98\x85\x98\x19p\xde\x99" + - "\x89\x9bP\xf9\x94s~" + - "\xb4\xc3\x83\x15(\xa1\x000oGv\x13m\xb6'K" + - "\xfe5\xe3\\{\x9d\xf9\xde\xb6\x97\xaa\x9c\x91\x9f\xfaq" + - "v1*52]--\xff\x18\xf0\x93\xa7\xef\xd9\xd9" + - "uz\xc1+\xea4LU\x0b\xbd_\xde\x84\xcaQ\xe2" + - "\x9dwXn$}F\x1a\xacb\xe7R\xff\xee\x92A" + - "T>\xbe\x84~\xbe\x7f\x09g_|\xc7\xf7\x1fH\x9f" + - "\xf9\xfe+\xd5*\xa5\xc0\x99\xf7\xb9b\xa3\"\xd7s\x97" + - "\xae\xff\xad\x00\xe8M;\xf4'\x7f\xdb^<\xf5\xf31" + - "\x82H\xb9e\xea\x07J\xd7T\xfa\xb5p*\xc9xz" + - "\xce\xe1??\xbb\xe3W'\x92\x9e\xb2\x7f*\xf7\xd8\xa3" + - "SIa\xf7|}h\xd3\xb2\xab\x86OV\x1b\x88s" + - "\x9e\x9c:\x8c\xca9\xbe\xddY\xbe\x9dpF\xfb\xda]" + - "\xff\xfe\x8d7\x12>{{\xc3[\x08)o\xd9\xca;" + - "\x06k\xee<}:y\xd0\xd2\x06n:\xad\x81\x0e:" + - "\"?\xa0\x1c\xdb\xff\xd7o\xd3AR\xb5\xba\xb76\xf4" + - "\xa2\xb2\xbb\x81~\xeejx\x82\x84\x8cbg,\xc7\xd9" + - "qY3*\xfb.\xa3{\xed\xb9\x8c\xee5\x7fu\x1b" + - "[u\xd3m\xef\x80gf;*\xb7\xcc\xe4\xee>\xf3\x9b\x8a>\x93o" + - "\xfe\x9d\x05\xcbonz\xf9\x83\xa4&\xd4\x99:s\x10\x95W\x89w\xde" + - "\xcff\xdeO\x17\xbd\xed\xad\x877\xb4\xfc\xe0\xa3OH" + - ".\xb1\x0a\xe7\x86.\xefEe\xc7\xe5\x1c\x8b/\xa7X" + - "Zr\xf0\xd47\x06v\x1f\xfftL\xe4\x9e\x7f\xc56" + - "T\xba\xae\xe0\x8et\x05\xa1\xd5_I{O\xdf\xf5\xdb" + - "?\xfb,)\xd5\x9c\xa6\xb7H\xaa\xb6&\x92j\xf3{" + - "{:\xef_u\xf0\x8b$\x83\xd6\xf4<1\xac\xe3\x0c" + - "Q0\x8e\xe5i\xbb\x9a\xdaQ\xd9\xdfD\xe7\xedkj" + - "\x819\x9e[1Mf\xd8\xe5T\xe1\x0f\xc3\x9f\x85\xb9" + - "\x05\xadl\x96\x9b\xdb*\xee\x003]\xbd\xa0\xb9\xac\x9b" + - "\xb58e\xcbtX\x0eQ\xcd\x8a)\x80\x14\x02\xc8\xda" + - " \x80\xbaZD\xd5\x10PF\xac'\xb4\x96u\"\x0e" + - "\x88\xa8\xba\x02\xca\x82PO\x88 \xafk\x02P\x0d\x11" + - "\xd5\x8d\x02\xa2XOx'W\x1e\x00P7\x8a\xa8n" + - "\x17\xd0+3\xbb\xa4\x99\xcc\x84\x8c\xbb\xd0\xb6\xb1\x16\x04" + - "\xac\x05\xf4l\xe6\xdaC\xda\x1a\x032,A\x96\x067" + - "\xb8X\x07\x02\xd6\x01z\x03V\xc5vV\x98.\xeaF" + - "7\xeb\xb3\x99\x83\x038\x01\x04\x9c\x008\x9ex\x1d\x96" + - "i\xb2\x82\x9b\xaf\x14\x0a\xccq\x00H\xb2\x89\x91d\xb3" + - "\x1e\x06P\xaf\x15Q\xbd)!\xd9|\x92\xecF\x11\xd5" + - "V\x01=\x87\xd9\xeb\x99\xbd\xc4\xc2\x82\xe6\xea\x96\xb9L" + - "\x13K,\xbav\xc1\xd0\x99\xe9vX\x901\xfb\xf4~" + - "\xcc\xc6\xa1\x00\x88\xd9\xf1/\xb6p\xa3\xee\xb8\xba\xd9\xdf" + - "\xc3\xe9-9\xcb\xd0\x0bCt\xbbZ\xae\xc9\x19\xcd\xb4" + - "\x87|i/\x00\x0a\xb2\xdc\x0e\xd0\xa2\xf7\x9b\x96\xcd\xbc" + - "\xa2\xee\x14H(\x10\x0b\xee\x965\x9a\xa1\x99\x05\x16\x1d" + - "4a\xf4A\xfe\x01y.\xc7\\-a\xed+s\x9a" + - "\xad\x89%G\xad\x8d\xf4\xb1\xb0\x17@] \xa2\x9aK" + - "\xe8c\xe9b\x00u\x89\x88\xeam\x09K\xafh\x07P" + - "s\"\xaa\xab\x04\xf4,[\xef\xd7\xcd\x0e\x06\xa2\x9d4" + - "\x98\xe3\x9aZ\x89\x01@\xa8\xb0-V\x99\x94\xe8`6" + - "F\xe9*M\xa5G\x0b\xd0\xc9\x0c\xc3\xba\xd5\xb2\x8d\xe2" + - "r\xff\x1c\x8b\xb4\xcdM\x19-\x93\xc6\xb0<7\x0e\xc9" + - "\xad\x17\xd8\xdc\x8a\xc3\xfcu\x15\x9b\x1b\xf2\xcan\xe6T" + - "\x0c\xd7\x01PS\x91\xf8u\xcd\x00\xeaD\x11\xd5z\x01" + - "[l\xce\x80\xd9\x18\xd4\xab\xaez>]WL\x9b\xf5" + - "\xeb\x8e\xcbl\x9f|e\x0b)\xbc\xe4$\x0f$\xff\xcb" + - "\x8a\xa8N\x17\xd0\xeb\xb7\xb5\x02\xcb1\x1bu\xab\xb8L" + - "3\xad\xbc\xc8\x0a\x98\x06\x01\xd3\xe3{\xd2\"M7X" + - "\xd1\x97nn\xa1\x91\xffO\xd1[\xeby~\xf8\xf6\xc6" + - "\xe1[\x87_xA\xfcn\x8a\xe3\xb7N\xf8\xdc\x1b\x1d" + - "\xc0u\xe2g^\x10\xc2\x14\x11\xae\x88\xea]\x14\x11\x95" + - "2\xe9\xd4\x01\xd1\xb21\x1b\xa3d\xa0\x1dV\xec'M" + - "\x9b\xd0\xc2\x0a\xa4h\xcc\x86\xd9\xdeg\x90\x8a\xd6\x00f" + - "\xe3R&Xf\xb3\xf5\xccvX\x0e2\xb6\xb5q\x08" + - "\xb3q\xd6\xaf\xd2\xfa\x94\x8b\xd5zh\xe8h\xd5\xf8\xeb" + - "mV\xf0!#X\x9ek\xf4\x8d\x96\x08\x12\xd2Q\xab" + - "\x88\xea\x92D\x90tQ\x90t\x8a\xa8\xf6$\x82Dm" + - "\x8f#\xa7\x0a\xcc~O\xb11\xe2\xea\xc1\xc5\xa3+'" + - "\xfc\x8c\\\xa0VD\xb5\x810\x98\xbe2\x97\xa2\x95N" + - "\x8b\xaa\xdb\xf3\x9f\xd6A\xff\x06\x88\x9a\x0bv\xb1\x03P" + - "m\x88\x0e\xdbC\x87=$\xa2\xfaXB?\xfbm\x00" + - "\xf5\x11\x11\xd5\x83\x02b\xa0\x9e\xa7\x0e\x00\xa8\x07ET" + - "\x9f\x15P\x16\x05\xdf\xd7\x8e\xce\xa6^HD\xf5\x17\x02" + - "\xca)\xb1\x9eJ}\xf9U\x8a\x93_\x88\xa8\xbe.\xa0" + - "\x9cN\xd5c\x1a@>\xb9\x06@=!\xa2\xfa\xe6\x97" + - "AP\xc1\xb0*\xc5>C\x83F\x9b\x15\xbb\x16Dt" + - "\xb3R\xca\xd9l\xbd\x8eV\xc5is]V\x92\xca\xae" + - "\x13f\x93\x8c\xab\xf5;8\x050'\"f\xe3\xfa\x10" + - "\x90\x88\xd1\x9eh\xb3\xe2Jf;\xbah\x99QB\xd0" + - "M\x97\x99\xee\x12\x0d\xa45\xcc\x88\xa8\xe3\x00Fw\xe0" + - "\xf6\xe4\xf4A\x04[1\xc8a?\xb9\xddt\xcf\x0b\x94" + - "\xb8pv\xecy3\xf0\x0b\"\x93\x1e\xbb\xbac\xdf\x9b" + - "!|Nd\xd2\xa4\xda\x1bCtf\xc0u\xcb\x98\x8d" + - "\xeb\xc6\xc0\xd8\x1b\xd8\x1a\xc7*\xace\x80\x84tQ\x11" + - "\x13|\x1d\x08\x90\x17D\xa3\x88\xd9\xb8\xef\xab\xf2\x14\xf1" + - "\xcb\xd2n\x0b%y\xcb\xe6Y-\x0e\x9f\x1bb!\xa2" + - "\xe8\xe9MFOk\x10=k\xe2\xfb7\x16\xb4\x8a\xc3" + - "F\xd6\x0bm}.\x88\xcc\x8e \xd2\x19\xb0*F\xb1" + - "\x9b\x81\xe4\xdaC\x88 \x8e\x0f\x9c\x0b\xac\xce\x84\xe2" + - "}7\x1e;\x17F\xa9\xb07\x99\x0a\x03\xf5\xaf \xf5" + - "\xf7\x88\xa8\x96\x05\xf4\x0c\x82\x1e\xb3\xd3\x02\xd1q\xa3\xeb" + - "\xfa\xc4\x9c\xc5\x9dS\x02\x01%@\xafRv\\\x9bi" + - "%\xc0\xc8\xdb\x88\x7f\xcaEd\x98*\xa4\xcbi\x99j" + - "\xa8\xfa\x7f\xcf\xe7\x17\x9f\x98\xfd$9\"-\x1fHd" + - "\xc9B\xb0\x1a\xf9\xf2\x0e\xcb\x94.\xba\xf4\x0a\x10\xccO" + - "\x0cs\x83DOUa\x981g\x11\xc2_)\xa2z" + - "]2c\xce!\x15]#\xa2z\xa3\x80\x12\xb3)\xf9" + - "E\xed\xbb\x7f\xe8\x16\xc7/31\x1b\xcff\xce\x7f\x9d" + - "D\x05\xae[\xe6(7\x1c3\xdb\xdc\x90\xb0kh\xc2" + - "\xa5k\x12\xd9f-\x1b\x0a\xad\xd4\xc8J\x9a\x1e\xa3Q" + - "`\xdc6\x90\xbe\x15\xf3\x8c[\xa9\x06\x19\xdd\xcf\xe7-" + - "\xbe\xb5\xe8\x92\xf5\xd1%\xef\x1c\x06P\xef\x12Q\xbd/" + - "q\xc9{\xa9\xf0\xbfOD\xf5\xa1\xc4%w\x93\x12w" + - "\x8a\xa8\xee%\xcc\x0fZ\x84=d\xe0\xbd\"\xaaO\x0a" + - "\x88)\x1f\xf2\x1f'\xc8\x7fRD\xf5\x88\xc0\x01\xbb\xb3" + - "\xad\xc321\xb8\x84\x03\x10\x15\xff\x03L\xb3\xdd5L" + - "C\xb7\xcbt\x99\xbd^C#\x84\x84-\xae^bV" + - "\xc5\x8d \xa2\xa4m\xe4\xd5\x12\x16;\xfdU\x92\xe6:" + - "X\x03\x02\xd6PD:\xcc\xee\xb0Y\x11\xc9\x1a\x9a\x91" + - "\xd3Dw\xe0B\x144\x12\xc43c\xa8\x87j\xad\xcd" + - "\"\xaa\xdf%(\xc1\xc4\x88H\xbe{\x10\x04\x8e$$" + - "\xf3\xba\xf6\xb8\xfa\xe2\x091]\xd5?\xf1\x848\x01@" + - "\xdeJ\xda\xd9.\xa2\xbaS\x08\xaf\xd6iA\x8b\x1f\xa1" + - "\xd5\xa6\x0e\xfa\x93-\x84\x9a:\x8b\xe5\x0d\xea\x05\x1d-" + - "\xb3\x87+\x0acM\x15\xacR\xd9&W\xd6-S\xad" + - "h\x86.\xbaC\xd1\xc2quA\x90\xe4\x87\xf2\xf2r" + - "#7\x16)\xe3\xc6P\x19\xca\xb7q1@~\x15\x8a" + - "\x98\x1f\xc0\xd8]\x14\x86\xed\x00\xf9\xd5D70\xf6\x18" + - "E\xc7i\x00\xf9\"\xd1\xcb\x18\xb5\x95J\x09\x9f\x06\xc8" + - "\x97\x89\xbc\x19\xe3RA\x19\xe2\xdbo$\xfav\x8c\xab" + - "\x05e+\xce\x06\xc8o&\xfaCD\x9f pM*" + - "\xbbq\x10 \xff \xd1\x1f!\xba\x94\xaeG\xde8\xa3" + - "\x0d\x90\xdfK\xf4'\x89>\xb1\xa1\x1e'\x02(\x8fs" + - "\xfacD?D\xf4\x9a\xaf\xd5c\x0d\x80\xf2#\xdc\x06" + - "\x90?H\xf4g\x89>\x09\xebq\x12\x80r\x14\x1f\x06" + - "\xc8?K\xf4\x9f\x12}\xf2\x84z\x9c\x0c\xa0\xbc\xcc\xef" + - "s\x8c\xe8\xc7\x89^\x9b\xaa\xc7Z\x00\xe5gx\x00 " + - "\x7f\x9c\xe8'0\xc2\xbb\xaeb\x12v\xc9\xdd\xf4\xb8\xec" + - "\x10-'29\x0b\xdaJ\xf4sB\xce\xcaP_\x89" + - "\x99x\xfe\x0b\x88\x19@\xaflY\xc6\xb2\x91p~\xbe" + - "\xca'p\x17\xc8XfW1\x8a?\xdf\xc9\x96X\xd0" + - "X\xd0\x8c\xaer\\\x0b9m\x15\xd7\xaa\x94\xa1\xb1\xa8" + - "\xb9\xac\x18%d\xbbb.\xb2\xadR\x0f2\xbb\xa4\x9b" + - "\x9a\x01\xd1\x97\xf1|.S\xa9\xe8\xc5h\xefq\x0b\xb8" + - "\xc8=\x85j\xf7l,7\xf7h\xfdU#\x80\xd91" + - "\xd6G\xd05\xe7\x86\x18\xea3\xc9\x90j\\\xaf\x19\x15" + - "v!\x95\xdd\xb8MEw\x8b\xdf\x94\x9c\xaf\xf7\x0c\x07" + - "V\xe7/\xcdWT\xa5Q?\xb9\x8d\x9aw\xb4\xc7\xc2" + - "F\xb2\xda\xc1\x0c\xa4S\x88\x13Xh\x92\xbe\xa0\xb7\x84" + - "F\xda;\xe1\x1c\xd1D1p\x8e\x0b\xd5D?s\xfd" + - "_]f\x9fE\xb9^\xd2J\xceW\\\xdd\xcd\x9c\xcc" + - "\x85h1\x9e\x11\x9e?\x19w\xf6\xf4\xe4\xe21\x83\xe8" + - "#\xf9u\x11x\xb5a7@\xbe\x95\xa2s\x09F:" + - "T\xba8\x88t\x12\xb9\x07\xe3\x12VQ9X\xe4\x88" + - "\xbe\x0a\xe3&G\xb9\x9d\x07y\x8c\x81\xa96\x1f\xbc\x18" + - "\xdf>\xc2:9\x8d>x\x95\xf8\xfe\x06\xd17&\xc1" + - "\xab\x82\xc3#\xc0N\x12}\xf0\xda\xcaAg;\xd1w" + - "r\xf0J\xf9\xe0\xb5\x03\x9f\x01\xc8\xef$\xfa^\x0e^" + - "i\x1f\xbc\xf6\xe0\xf3#\xc0n\xd2\x04\x1f\xbc\x1e\xe7\xfc" + - "O\x12\xfd\x08\x07\xafv\x1f\xbc\x0es\xb0;D\xf4c" + - "\x04R\x15\xdb\xc8\xbb\xb6n\x02\xf6\xc7\xb1Q(\x7f\x8b" + - "\xb1r\x1bd\x0c}=\x8b\x12KQ\xd7\x8c\x05\x15\xcd" + - "\x80\xc6\xbc\xab\x15\xd6\xc6u\xba\xe1tjf\xd1\xc1\x01" + - "m-\xa3t$%\x13\xb7k8+\x99\xad\xf7\x01\xc6" + - "\x95}T\xc8dr\x96U]\xdf\xf0\x02\x91\xd9>\xc2" + - "E\xdfJ\xda\xc6\xae\xa2\xc1:0,gD3N\x87" + - ":}\xb1L\x13\xfd\x1a\xa3Go\x1cY<\x94\x83^" + - "!,BzZ\xaa\xaa\x0b\xb6\xb1\xcc\x0an\x87\x85\xa6" + - "\xab\x9b\x156j\x83\xc2@\xc5\\\xcb\x8a\x0b\xd1,X" + - "E\xdd\xec\x87QM\x8a\xf8e\xd3\x9dD\xd5\xc5\xa3\x19" + - "\x13\xafc\xf2\xacf\x108tQ\x0d!7\xc7\xad~" + - "K\x81\xafj\xb1\x99\xe6$\xba\xd4qN\x0b\xa6\x91~" + - "\x90\xf9m}\x1a zJ\xc2p\x1c/\x1f\xde\x04\x82" + - "\xfc\x94\x84\xf1+\x06\x86\x8f\x16\xf2>\x1b\x04y\xb7\x84" + - "B\xf4\xc4\x87\xe1\xf3\x9c|\xef0\x08\xf2\xdd\x12\x8a\xd1" + - "\xb3\x1b\x86\xb3ny\xa8\x1d\x04\xb9$a*z\x82\xc4" + - "pP.kT'\xdd.a:z\xcf\xc3\xf05F" + - "^\xba\x0d\x04y\xa1\xe4\x85\xed\x10\xb4\xf8b\xb4\xa2\x17" + - "\x02\x064r\xc8hE/\x9c\x0fa\xd86\x01\xb4\xe2" + - "\x96\x00\x9e[\xd1\x0b'\xa4\x90)h.k\xa5^\xd3" + - "\xff\x88\x01xC+&'\x8f\xe2\x9758c\x17\xca" + - "\xedq1\x17\x02\xf0\xd6\xe1\xb8\x96\x8b\x9a\xca\x1dO'" + - "\xeb\xe4`6\xb2g[0Y9\x92\x98\x8d\x1c\xa6\xe2" + - "\xf9\x88\x88\xea/\x85\xb82\x08}:\x1c\xd6\xa1e\x87" + - "]\xee83\xbb\xc0\xf3\x83\x1a\xb6zr\xe7\x15\xad\x01" + - "^\xe3\xa2\xbf\x95\x03q:H\x8e\xf3\xa6$\xc6y\x18" + - "\xf6\xd7\xd2\x88\xec\x91\x1c\xeeM9O\xb3\x96\xec\x16y" + - ":Kq\x97\x0c\x1f/1|g\x96er\xad:\xc9" + - "\x0b;J\x0cs!T\x99\xec\"\xdb\xean\xd6\xf8\x7f" + - "I\xd6c8\x88\x7fN\x86<\xd2\x17(\xdaw01" + - "\xa73\xac\xa0#\xcc,K\x16\xf5\xe3\xe8\xca\xbfpX" + - "\x82gh1\xed?=\xda\xffhS0\\;\x96(" + - "v\x9ek\x0a\x1c\xe8\xa5D\x9f\xf6\xc2b\x00\xf5\x98?" + - "q\x0b_rN\x92\xa3\xbe.\xa2\xfav\xc2\xfd~G" + - "\x8co\x8a\xa8\xbe\x1b\xe7+\xf9,\xf5,\xef\x8a\xa8\xfe" + - "7%\xab\x94\xdf\xb3|L\xfd\xe9G\"vc\xd0?" + - "\x87\xcf<\x15;Fo\xc3\xea_\xa2\x9b\xcc\xa1\xb2\xb4" + - "j*\x12\xbe\x1d\xa1K\x98X\xb1\x09\xd8G\x02h\xd7" + - "\x82D5\x1b\x0d\x89\x90\xd9y\x8a\xe1\":\xd1\xf0\x85" + - "\xadg\xa6\xbb@\xef\x07\x899\xf1\x88c\x1c\xd5\xe6\x83" + - "@\xf2\xe3(\xa8\x0b\x12]\xfa\x81\xc4\x00+T\xac\xfa" + - "|0\x18Z\x9dP\xec\xb7I\x0b\xabDT\x07\x04\x0e" + - "6\xd6\x8arQC\x97-\xb2\xd9\xba\x0a\x93\xcc\xc2P" + - "\xdc\xadR\xbfVpV`\x99*\xe8E6kYW" + - "aI\x86\xf0\xc5\x00$\xdd*\x8ez*\x18\xa3J\xbc" + - "\x95\xad\xc9[\x85\xb5\xcc\x1d\xf1\x92R\xf5\xda\xd7\x1d?" + - "\x17D\x8f}\xdd\xc9\xc7\xbe\x00\xa2\xd6\x91\xb3\x96ET" + - "7' jh8nu\xc7.\x0b~?\x99\xfc+" + - "=xQQ,]H\xc1\x18\xfd\x1d\xceW\x9c\xc0_" + - "h}\x1f?\xe3^\xe4\xd4\x0a\"\xdc\xc0\xc4\x9fi\xd0" + - "!B\xb0\xf9\xff\x06\x00\x00\xff\xff\"\xcf\xdc\xc0" +const schema_db8274f9144abc7e = "x\xda\xccZ}\x90\x14ez\x7f\x9e\xeeY\x9a\x85]" + + "f:=\x96+\x02#[\x12\x85\x13\xa2\"\x89\xb7I" + + "n\xf6\x03\xb8]\x8e\x8f\xe9\x9d\x05\xbd\x95K\xd1;\xf3" + + "\xeen/=\xddCw\x0f\xb0\x1b8\x84\xc2xl\xe4" + + "\x04OR\xe2\xe1\x95\xa0\xc4\x8fp9\xf0\xa0\xa2\x06-" + + "M\xee\"\xe6\x8ex\\ %\x17\xadS\xd1J\x9d\xa5" + + "eP)c\xca\xb3SO\x7f\xef\xec\xba\x80I\xaa\xee" + + "\x1f\x98z\xfay?\x9e\xaf\xdf\xf3\xf1\xee\x8dC\x93\x9a" + + "\xb9\x9bj\xaeK\x02\xc8\x87k&8l\xce/\x87\x1e" + + "\x9e\xf5\x8f\xdb@\x9e\x8a\xe8|\xfb\xf8\x92\xf4\xa7\xf6\xb6" + + "\x7f\x87\x1a^\x00\x98?(\x0c\xa1\xb4S\x10\x00\xa4\x1d" + + "\xc2\x7f\x00:5\xbf\x7f\xe6\x8d\xf2\x1b\xc2v\x10\xa7\xc6" + + "\x999b.M\\\x82\xd2\xd6\x89\xc4\xbcy\xe2\x06@" + + "\xe7OJ\xaf\x1c\xf8\xc3=?#f.b\x06\x9c\xff" + + "\xce\xc4!\x94>u9/L\\\x01\xe8|t\x7f\xc3" + + "\xdf\xec\xff\x97\x13w\x81x\x1d\x82\x7fv}\xed\xaf\x10" + + "P\x9aY\xfb#@\xe7_o\xdc\xf4\xf6\x9a\x8fv\x7f" + + "g\xe4\xb9\x09\xe2{\xb1v\x18\xa5\xb3\xb5\x02\xf0\xceC" + + "w\xa4\xff\x19\x1f\xfed7\x88\xd7\xd36H\x9f\x8f\xd5" + + "N\xe2\x00\xa5\x93\xb5Y@\xe7\x95\x1b\x8e?\xbb\xeb\xc7" + + "w\x7f\x1f\xe4\xeb\x10\xc1[\xff~\xed\x7f\xd398\x89" + + "\x18\xce?\xfa\x95\xc4\x0f_\xf9\xbd\x1f\xb8\x0c\xce\xc1S" + + "\xb7=\xb5\xeb\xc7\xd7\xbc\x0b+9\x01\x13\x00\xf3gO" + + "2\x89w\xc1$\xd2\xc5\xfd\xaf>\xb7\xbc\xb4\xfb\xc1\x03" + + "\xde\xa5\xdd\xbd\xae\x98\xccq\x90p\xb6w|RZ\xf9" + + "H\xfe\x11_\x1c\xf7S\xed\xe4\x0fi\xe9\xf4\xc9\xb4t" + + "\xc1\xaf\xdeY\xb1\xec\xa9\xde\xc7|\x06\xf7\xa2\x9fN~" + + "\x8a\x18j\xeb\xe8\x1e'6\xa4\xeei\xf9\xa3{\x1f\xab" + + "\xb6J\x0dq\xce\xad\x1bFiQ\x1d\xfdl\xa9\xbb\x0d" + + "\x01\x9d\xe1\xaf>\xb7\xea\xa3\xbf\xb0\x9e\x04y.&\x9c" + + "\x9f\xec8\xb7~\xf6\x13\xbd/\xb9\xd7\xe6\x01\xe6?S" + + "\xffK\xda\xfad=\xa9\xb2\xfe\xef\xe7,\xbf\xf7\xed\xa5" + + "Gh\xeb\x98Y\xbcK\x94\xa64\xa1\xb4y\x0aYf" + + "p\x0aq\xff\xe2\x86U\xcf?\x7f\xb8\xefH\xf5E\\" + + "\x8b_\x95\\\x82\xd2\xdc$q\xcfN\x12\xf7\x15\x1d\xf8" + + "\xda\x0b7%\xfe.n\xc7\xd7\x93\xef\xd2\xe1\xe7]\x86" + + ";>;\xf6\x0f\x8b>8\xfdL\xdcB\xbbS\x1cY" + + "\xe8`\x8a\x04\xef\x1e\xc6\xd2kM\xcd\xcf\x83|=\xa2" + + "3\xb0g\x93\xdd\xfe\xc0N\x07V\xa2\x80\x1c\xc0\xfc\x93" + + "\xa9!\xda\xecl\x8a\xfck\xfa\xfb\xad\xf5\xfa\x07\xdb^" + + "\xa8rF\xf7\xd4\x05\xe2\x12\x94:D\xba\xda\"\xf1G" + + "\x80\x9f" + + "\xf4y\x9ca\x7f\xe3\xb3\xc4p\xc4e\x08\x83q,O" + + ";\xd5\xd8\x8a\xd2\x9b\x8dt\xde\xeb\x8dY\x98\xeb\xd8\x15" + + "]g\x9aYN\x14\xfe \xf8Y\x98WP\xcaz\xb9" + + "\xa9\xa5b\xf73\xddV\x0b\x8a\xcd:Y\xd6*\x1b\xba" + + "\xc5r\x88r\x8aO\x00$\x10@T\x06\x00\xe45<" + + "\xca\x1a\x87\"b\x9a\xd0ZT\x89\xd8\xcf\xa3ls(" + + "r\\\x9a\x10A\\\xd7\x08 k<\xca\x1b9D>" + + "Mx'V\xee\x03\x907\xf2(o\xe7\xd0)3\xb3" + + "\xa4\xe8L\x87\xa4\xbd\xc84\xb1\x0e8\xac\x03tLf" + + "\x9b\x83J\x8f\x06I\x16#\x0b\x03\x1bl\xac\x07\x0e\xeb" + + "\x01\x9d~\xa3bZ+u\x1bU\xad\x93\xf5\x9a\xcc\xc2" + + "~\x9c\x00\x1cN\x00\x1cO\xbc6C\xd7Y\xc1\xceW" + + "\x0a\x05fY\x00$\xd9\xc4P\xb2\xd9\x0f\x02\xc87\xf0" + + "(\xdf\x1a\x93l\x01Iv\x0b\x8fr3\x87\x8e\xc5\xcc" + + "\xf5\xcc\\j`A\xb1UC_\xae\xf0%\x16^\xbb" + + "\xa0\xa9L\xb7\xdb\x0cH\xea\xbdj\x1f\xa6\xa2P\x00\xc4" + + "\xd4\xf8\x17[\xb4Q\xb5lU\xef\xebr\xe9\xd9\x9c\xa1" + + "\xa9\x85A\xba]\x9d\xab\xc9\xe9M\xb4\x87xE7\x00" + + "r\xa2\xd8\x0a\x90U\xfbt\xc3dNQ\xb5\x0a$\x14" + + "\xf0\x05{K\x8f\xa2)z\x81\x85\x07M\x18}\x90w" + + "@\xde\x95c\x9e\x12\xb3\xf6\xb59\xc5T\xf8\x92%\xd7" + + "\x85\xfaX\xd4\x0d /\xe4Q\xce\xc5\xf4\xb1l\x09\x80" + + "\xbc\x94G\xf9\xf6\x98\xa5W\xb6\x02\xc89\x1e\xe5\xd5\x1c" + + ":\x86\xa9\xf6\xa9z\x1b\x03\xde\x8c\x1b\xcc\xb2u\xa5\xc4" + + "\x00 P\xd8\x16\xa3LJ\xb40\x15\xa1t\x95\xa6j" + + "F\x0b\xd0\xce4\xcd\xb8\xcd0\xb5\xe2\x0a\xef\x1c\x83\xb4" + + "\xed\x9a2\\&\x8cay\xd78$\xb7Z`\xf3*" + + "\x16\xf3\xd6UL\xd7\x90\xd7v2\xab\xa2\xd9\x16\x80\x9c" + + "\x08\xc5\xafo\x02\x90'\xf2(\xa79\xcc\x9a.\x03\xa6" + + "\"P\xaf\xba\xea\xc5t]\xd1M\xd6\xa7Z63=" + + "\xf2\xb5YRx\xc9\x8a\x1fH\xfe\x97\xe2Q\x9e\xc6\xa1" + + "\xd3g*\x05\x96c&\xaaFq\xb9\xa2\x1by\x9e\x15" + + "\xb0\x068\xac\x19\xdf\x93\x16+\xaa\xc6\x8a\x9et\xf3\x0a" + + "\x19\xf7\x7f\x8a\xde:\xc7\xf1\xc2\xb7;\x0a\xdfz\xfc\xdc" + + "\xf1\xe3w(\x8a\xdfz\xee\xb7\xce\xe8\x00\xae\xe7?s" + + "\xfc\x10\xa6\x88\xb0y\x94\xef\xa4\x88\xa8\x94I\xa7\x16\xf0" + + "\x86\x89\xa9\x08%}\xed\xb0b\x1fiZ\x87,+\x90" + + "\xa21\x15d{\x8fA(\x1a\xfd\x98\x8aJ\x19\x7f\x99" + + "\xc9\xd63\xd3b9H\x9a\xc6\xc6ALEY\xbfJ" + + "\xebS.W\xeb\x81\xa1\xc3U\xe3\xaf7Y\xc1\x83\x0c" + + "\x7fy.\xe3\x19-\x06\x87\xa4\xa3\xd5<\xca\xfd\xb1 " + + "a=\x00r\x91G\xb9\x1c\x0b\x92\xd2\x92H\x9b\"\x1f" + + "\xe0!EN\x99Gy\x137\x12\xe1\xd8z\xa6\xdb\x0b" + + "\xd5>\x10\x98\xf5\xff\x10F#\xa4\xf4e\x0c\xa5\x8b\xb9" + + "$yK\x1d\x8fr\x03\xc15}e6\x056\x9d\x16" + + "\x16\xc2\x17?\xad\x8d\xfe\xf5\xc17\xe7\xefb\xfa\xf8\xdb" + + "\x10\x1e\xb6\x97\x0e{\x80G\xf9\xd1\x98*\xf7\x9b\x00\xf2" + + "\xc3<\xca\x878D_\x93O\x1c\x00\x90\x0f\xf1(?" + + "M\x9a\xe4\xa1\x9f)\xa6\xdd\xc3\x14\xb4;t\x9b\x99\xeb\x15\xd4" + + "\x02H\xd8b\xab%fT\xec\x10\"J\xcaF\xb7\xb0" + + "\xc2b\xbb\xb7JPl\x0bk\x81\xc3Z\x8aH\x8b\x99" + + "m&+\"YC\xd1r\x0ao\xf7_\x8a\x82F\x82" + + "xr\x0c\xf5PY\xb6\x89G\xf9;\x04%\x18\x9b&" + + "\x89w\x0d\x00\xe7\"\x09\xc9\xbc\xae5^Zp^\x9a" + + "\x8b\xb7ZnB\x9c\x00 n%\xedl\xe7Q\xde\xc5" + + "\x05Wk7 \xebEh\xb5\xa9\xfdVf\x0b\xa1\xa6" + + "\xca\"y\xfdzAEC\xefr\x15\x85\x91\xa6\x0aF" + + "\xa9l\x92+\xab\x86.W\x14M\xe5\xed\xc1p\xe1\xb8" + + "\xba H\xf2ByE9\xe3\x1a\x8b\x94qK\xa0\x0c" + + "\xe9[\xb8\x04 \xbf\x1ay\xcc\xf7c\xe4.\x12\xc3V" + + "\x80\xfc\x1a\xa2k\x18y\x8c\xa4\xe2T\x80|\x91\xe8e" + + "\x0c;P\xa9\x84O\x02\xe4\xcbD\xde\x84Q\xa9 \x0d" + + "\xba\xdbo$\xfav\x8c\xaa\x05i+\xce\x01\xc8o\"" + + "\xfa\x03D\x9f\xc0\xb9\x9a\x94\xf6\xe0\x00@\xfe~\xa2?" + + "Lt\xa1&M\xed\xb6\xf4\x10\x9a\x00\xf9}D\x7f\x9c" + + "\xe8\x13\x1b\xd28\x11@:\xe8\xd2\x1f%\xfaa\xa2\xd7" + + "^\x95\xc6Z\x00\xe9\x87\xb8\x0d \x7f\x88\xe8O\x13}" + + "\x12\xa6q\x12\x80t\x0c\x1f\x04\xc8?M\xf4\x9f\x10}" + + "\xf2\x844N\x06\x90^t\xefs\x9c\xe8'\x88^\x97" + + "Hc\x1d\x80\xf4S<\x00\x90?A\xf4\xd3\x18\xe2]" + + "G1\x0e\xbb\xe4njTv\xf0\x86\x15\x9a\x9c\xf9\x1d" + + "(z9!g$\xa9\x05\xc5d4*\x06\xc4$\xa0" + + "S6\x0cm\xf9H8\xbfX\xe5\xe3\xbb\x0b$\x0d\xbd" + + "\xa3\x18\xc6\x9f\xe7dK\x0d\xc8\x14\x14\xad\xa3\x1c\xd5B" + + "VK\xc56*e\xc8\x14\x15\x9b\x15\xc3\x84lV\xf4" + + "\xc5\xa6Q\xeaBf\x96T]\xd1 \xfc2\x9e\xcf%" + + "+\x15\xb5\x18\xee=n\x01\x17\xba'W\xed\x9e\x99r" + + "S\x97\xd2W5-\x98\x13a}\x08]so\x8e\xa0" + + ">\x19\x0f\xa9\xcczE\xab\xb0K\xa9\xec\xc6\xed?:" + + "\xb3^\xffr\xb165\x98m]\xbc4_Y\x95F" + + "\xbd\xe46j4\xd2\x1a\x09\x1b\xcaj\xfa\xe3\x92v." + + "J`\x81Iz\xfd6\x142\xb4w\xcc9\xc2\xe1\xa3" + + "\xef\x1c\x97\xaa\x89>f{\xbf:\xf4^\x83r\xbd\xa0" + + "\x94\xac/\xb9\xba\x93Y\xc9K\xd1b4N\xbcx2" + + "n\xef\xea\xcaE\x13\x09\xdeC\xf2\x1bC\xf0j\xc1N" + + "\x80|3E\xe7R\x0cu(u\xb8 \xd2N\xe4." + + "\x8cJXIv\xc1\"G\xf4\xd5\x1859\xd27\xdd" + + " \x8f00\xd1\xe2\x81\x17s\xb7\x0f\xb1N\xacA\x0f" + + "\xbcJ\xee\xfe\x1a\xd17\xc6\xc1\xab\x82\xc3#\xc0N\xe0" + + "=\xf0\xda\xea\x82\xcev\xa2\xefr\xc1+\xe1\x81\xd7N" + + "|\x0a \xbf\x8b\xe8\xfb\\\xf0\xaa\xf1\xc0k/>;" + + "\x02\xec&M\xf0\xc0\xeb\xa0\xcb\xff8\xd1\x8f\xba\xe0\xd5" + + "\xea\x81\xd7\x11\x17\xec\x0e\x13\xfd8\x81T\xc5\xd4\xf2\xb6" + + "\xa9\xea\x80}Ql\x14\xca\xdf`\xac\xdc\x02IM]" + + "\xcf\xc2\xc4RT\x15maE\xd1 \x93\xb7\x95\xc2\xda" + + "\xa8N\xd7\xacvE/Z\xd8\xaf\xace\x94\x8e\x84x" + + "\xe2\xb65k\x153\xd5^\xc0\xa8\xb2\x0f\x0b\x99d\xce" + + "0\xaa\xeb\x1b\xb7@d\xa6\x87p\xe1\xb7\x92\xb2\xb1\xa3" + + "\xa8\xb16\x0c\xca\x19^\x8f\xd2\xa1J_\x0c]G\xaf" + + "\xc6\xe8R3#\x8b\x87\xb2\xdf+\x04EHW\xb6\xaa" + + "\xba`\x1b\xcb\xac`\xb7\x19\xa8\xdb\xaa^a\xa36(" + + "\xf4W\xf4\xb5\xac\xb8\x08\xf5\x82QT\xf5>\x18\xd5\xa4" + + "\xf0_4\x08\x8aU]n4c\xec!M\x9c\xdd\x04" + + "\x9c\x0b]TC\x88MQ\xab\x9f-\xb8\xab\xb2&S" + + "\xacX\x97:\xcei\xfe\xe0\xd2\x0b2\xaf\xad\xaf\x01\x08" + + "_\x9d0\x98\xdc\x8bG\x86\x80\x13\x9f\x100z\xf0\xc0" + + "\xe0}C|\xc8\x04N\xdc# \x17\xbe\x06b\xf0\x92" + + "'\xee\x18\x06N\xbcK@>|\xa1\xc3`,.\x0e" + + "\xb6\x02'\x96\x04L\x84\xaf\x95\x18\xcc\xd4E\x85\xea\xa4" + + "o\x0aX\x13>\xfda\xf0p#.\xdb\x06\x9c\xb8H" + + "p\x82v\x08\xb2\x9e\x18\xcd\xe8\x04\x80\x01\x19\x172\x9a" + + "\xd1\x09FI\x18\xb4M\x00\xcd\xb8\xc5\x87\xe7ft\x82" + + "a*$\x0b\x8a\xcd\x9a\xa9\xd7\xf4>\xa2\x0f\xde\xd0\x8c" + + "\xf1!%\xffE\x0d\xce\xd8\x85rkT\xcc\x05\x00\xbc" + + "u8\xaa\xe5\xc2\xa6r\xe7\x93\xf1:\xd9\x9f\x8d\xec\xdd" + + "\xe6OV\x8e\xc6f#G\xa8x>\xca\xa3\xfc\x0b." + + "\xaa\x0c\x02\x9f\x0e\xe6zh\x98A\x97;\xcex\xcf\xf7" + + "|\xbf\x86\xad\x1e\xf29E\xa3\xdf\xadq\xd1\xdb\xca\x82" + + "(\x1d\xc4'\x7fSb\x93?\x0c\xfakaD\xf6\x88" + + "\xcf\x01\xa7\\\xa4Y\x8bw\x8bn:K\xb8.\x19\xbc" + + "sb\xf0$-\x8a\xe4Z\xf5\x82\x13t\x94\x18\xe4B" + + "\xa82\xd9e\xb6\xd5\x9d,\xf3\xbfI\xd6c8\x88w" + + "N\x92<\xd2\x13(\xdcw 6\xa7\xd3\x0c\xbf#L" + + ".\x8f\x17\xf5\xe3\xe8\xca\xbbpP\x82'i1\xed?" + + "-\xdc\xffX\xa3?\\;\x1e+v\x9ei\xf4\x1d\xe8" + + "\x85X\x9f\xf6\xdc\x12\x00\xf9\xb87q\x0b\x1e}\xce\x90" + + "\xa3\xbe\xca\xa3\xfcv\xcc\xfd\xde$\xc67x\x94\xdf\x8b" + + "\xf2\x95\xf8\x1b\xeaY\xde\xe3Q\xfe/JV\x09\xafg" + + "\xb9@\xfd\xe9\xc7 Date: Tue, 3 Dec 2019 04:29:40 -0800 Subject: [PATCH 17/21] bug(cloudflared): nil pointer deference on h2DictWriter Close() (#154) Unlike other h2DictWriter methods, the Close() method does check whether w.comp is nil. This PR adds a check for non nil compressor before attempting to close Bug: #141 --- h2mux/h2_dictionaries.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/h2mux/h2_dictionaries.go b/h2mux/h2_dictionaries.go index bd091894..bf92d58d 100644 --- a/h2mux/h2_dictionaries.go +++ b/h2mux/h2_dictionaries.go @@ -542,7 +542,10 @@ func (w *h2DictWriter) Write(p []byte) (n int, err error) { } func (w *h2DictWriter) Close() error { - return w.comp.Close() + if w.comp != nil { + return w.comp.Close() + } + return nil } // From http2/hpack From 8f4fd70783c7389eb9f48b0466c1a110b1ae2748 Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Thu, 21 Nov 2019 12:10:44 -0600 Subject: [PATCH 18/21] TUN-2606: add DialEdge helpers --- connection/connection.go | 11 +------- connection/dial.go | 54 ++++++++++++++++++++++++++++++++++++++++ connection/manager.go | 31 +++-------------------- go.mod | 1 + origin/supervisor.go | 2 +- origin/tunnel.go | 37 +++++++-------------------- 6 files changed, 70 insertions(+), 66 deletions(-) create mode 100644 connection/dial.go diff --git a/connection/connection.go b/connection/connection.go index 65731327..629d37e7 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -2,7 +2,6 @@ package connection import ( "context" - "net" "time" "github.com/cloudflare/cloudflared/h2mux" @@ -20,20 +19,12 @@ const ( openStreamTimeout = 30 * time.Second ) -type dialError struct { - cause error -} - -func (e dialError) Error() string { - return e.cause.Error() -} - type Connection struct { id uuid.UUID muxer *h2mux.Muxer } -func newConnection(muxer *h2mux.Muxer, edgeIP *net.TCPAddr) (*Connection, error) { +func newConnection(muxer *h2mux.Muxer) (*Connection, error) { id, err := uuid.NewRandom() if err != nil { return nil, err diff --git a/connection/dial.go b/connection/dial.go new file mode 100644 index 00000000..dc1e4c91 --- /dev/null +++ b/connection/dial.go @@ -0,0 +1,54 @@ +package connection + +import ( + "context" + "crypto/tls" + "net" + "time" + + "github.com/pkg/errors" +) + +// DialEdge makes a TLS connection to a Cloudflare edge node +func DialEdge( + ctx context.Context, + timeout time.Duration, + tlsConfig *tls.Config, + edgeTCPAddr *net.TCPAddr, +) (net.Conn, error) { + // Inherit from parent context so we can cancel (Ctrl-C) while dialing + dialCtx, dialCancel := context.WithTimeout(ctx, timeout) + defer dialCancel() + + dialer := net.Dialer{} + edgeConn, err := dialer.DialContext(dialCtx, "tcp", edgeTCPAddr.String()) + if err != nil { + return nil, newDialError(err, "DialContext error") + } + tlsEdgeConn := tls.Client(edgeConn, tlsConfig) + tlsEdgeConn.SetDeadline(time.Now().Add(timeout)) + + if err = tlsEdgeConn.Handshake(); err != nil { + return nil, newDialError(err, "Handshake with edge error") + } + // clear the deadline on the conn; h2mux has its own timeouts + tlsEdgeConn.SetDeadline(time.Time{}) + return tlsEdgeConn, nil +} + +// DialError is an error returned from DialEdge +type DialError struct { + cause error +} + +func newDialError(err error, message string) error { + return DialError{cause: errors.Wrap(err, message)} +} + +func (e DialError) Error() string { + return e.cause.Error() +} + +func (e DialError) Cause() error { + return e.cause +} diff --git a/connection/manager.go b/connection/manager.go index bb8d5e23..ee4b4870 100644 --- a/connection/manager.go +++ b/connection/manager.go @@ -4,7 +4,6 @@ import ( "context" "crypto/tls" "fmt" - "net" "sync" "time" @@ -128,12 +127,12 @@ func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurab } func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError { - edgeIP := em.serviceDiscoverer.Addr() - edgeConn, err := em.dialEdge(ctx, edgeIP) + edgeTCPAddr := em.serviceDiscoverer.Addr() + configurable := em.state.getConfigurable() + edgeConn, err := DialEdge(ctx, configurable.Timeout, em.tlsConfig, edgeTCPAddr) if err != nil { return retryConnection(fmt.Sprintf("dial edge error: %v", err)) } - 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{ @@ -148,7 +147,7 @@ func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError { retryConnection(fmt.Sprintf("couldn't perform handshake with edge: %v", err)) } - h2muxConn, err := newConnection(muxer, edgeIP) + h2muxConn, err := newConnection(muxer) if err != nil { return retryConnection(fmt.Sprintf("couldn't create h2mux connection: %v", err)) } @@ -196,28 +195,6 @@ func (em *EdgeManager) serveConn(ctx context.Context, conn *Connection) { 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{} - 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." + diff --git a/go.mod b/go.mod index c55ae1b9..bce9d655 100644 --- a/go.mod +++ b/go.mod @@ -57,6 +57,7 @@ require ( golang.org/x/sync v0.0.0-20190423024810-112230192c58 golang.org/x/sys v0.0.0-20191008105621-543471e840be golang.org/x/text v0.3.2 // indirect + google.golang.org/appengine v1.4.0 // indirect google.golang.org/genproto v0.0.0-20191007204434-a023cd5227bd // indirect google.golang.org/grpc v1.24.0 // indirect gopkg.in/coreos/go-oidc.v2 v2.1.0 diff --git a/origin/supervisor.go b/origin/supervisor.go index ff7d96f9..6c58674f 100644 --- a/origin/supervisor.go +++ b/origin/supervisor.go @@ -183,7 +183,7 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign return // try the next address if it was a dialError(network problem) or // dupConnRegisterTunnelError - case dialError, dupConnRegisterTunnelError: + case connection.DialError, dupConnRegisterTunnelError: s.replaceEdgeIP(0) default: return diff --git a/origin/tunnel.go b/origin/tunnel.go index 81605025..b6c2ea68 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -15,6 +15,7 @@ import ( "time" "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" + "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/streamhandler" @@ -27,7 +28,6 @@ import ( "github.com/google/uuid" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus" - _ "github.com/prometheus/client_golang/prometheus" log "github.com/sirupsen/logrus" "golang.org/x/sync/errgroup" rpc "zombiezen.com/go/capnproto2/rpc" @@ -76,14 +76,6 @@ type TunnelConfig struct { OriginUrl string } -type dialError struct { - cause error -} - -func (e dialError) Error() string { - return e.cause.Error() -} - type dupConnRegisterTunnelError struct{} func (e dupConnRegisterTunnelError) Error() string { @@ -214,11 +206,11 @@ func ServeTunnel( tags["ha"] = connectionTag // Returns error from parsing the origin URL or handshake errors - handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr.String(), connectionID) + handler, originLocalIP, err := NewTunnelHandler(ctx, config, addr, connectionID) if err != nil { errLog := logger.WithError(err) switch err.(type) { - case dialError: + case connection.DialError: errLog.Error("Unable to dial edge") case h2mux.MuxerHandshakeError: errLog.Error("Handshake failed with edge server") @@ -470,7 +462,7 @@ var dialer = net.Dialer{} // NewTunnelHandler returns a TunnelHandler, origin LAN IP and error func NewTunnelHandler(ctx context.Context, config *TunnelConfig, - addr string, + addr *net.TCPAddr, connectionID uint8, ) (*TunnelHandler, string, error) { originURL, err := validation.ValidateUrl(config.OriginUrl) @@ -491,22 +483,11 @@ func NewTunnelHandler(ctx context.Context, if h.httpClient == nil { h.httpClient = http.DefaultTransport } - // Inherit from parent context so we can cancel (Ctrl-C) while dialing - dialCtx, dialCancel := context.WithTimeout(ctx, dialTimeout) - // TUN-92: enforce a timeout on dial and handshake (as tls.Dial does not support one) - plaintextEdgeConn, err := dialer.DialContext(dialCtx, "tcp", addr) - dialCancel() + + edgeConn, err := connection.DialEdge(ctx, dialTimeout, config.TlsConfig, addr) if err != nil { - return nil, "", dialError{cause: errors.Wrap(err, "DialContext error")} + return nil, "", err } - 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 h.muxer, err = h2mux.Handshake(edgeConn, edgeConn, h2mux.MuxerConfig{ @@ -519,9 +500,9 @@ func NewTunnelHandler(ctx context.Context, CompressionQuality: h2mux.CompressionSetting(config.CompressionQuality), }, h.metrics.activeStreams) if err != nil { - return h, "", errors.New("TLS handshake error") + return nil, "", errors.Wrap(err, "Handshake with edge error") } - return h, edgeConn.LocalAddr().String(), err + return h, edgeConn.LocalAddr().String(), nil } func (h *TunnelHandler) AppendTagHeaders(r *http.Request) { From bbf31377c22420c4e86113a9b98a7b3f617e56cc Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Thu, 21 Nov 2019 11:03:13 -0600 Subject: [PATCH 19/21] TUN-2607: add RPC stream helpers --- connection/connection.go | 30 +++-------------- connection/manager.go | 24 +++++++------- connection/manager_test.go | 8 ++--- connection/rpc.go | 49 ++++++++++++++++++++++++++++ origin/supervisor.go | 3 +- origin/tunnel.go | 66 +++++++------------------------------- 6 files changed, 83 insertions(+), 97 deletions(-) create mode 100644 connection/rpc.go diff --git a/connection/connection.go b/connection/connection.go index 629d37e7..7e032745 100644 --- a/connection/connection.go +++ b/connection/connection.go @@ -4,15 +4,12 @@ import ( "context" "time" - "github.com/cloudflare/cloudflared/h2mux" - "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" - rpc "zombiezen.com/go/capnproto2/rpc" + "github.com/cloudflare/cloudflared/h2mux" + tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( @@ -41,32 +38,15 @@ func (c *Connection) Serve(ctx context.Context) error { } // Connect is used to establish connections with cloudflare's edge network -func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (pogs.ConnectResult, error) { - openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout) - defer cancel() - - rpcConn, err := c.newRPConn(openStreamCtx, logger) +func (c *Connection) Connect(ctx context.Context, parameters *tunnelpogs.ConnectParameters, logger *logrus.Entry) (tunnelpogs.ConnectResult, error) { + tsClient, err := NewRPCClient(ctx, c.muxer, logger.WithField("rpc", "connect"), openStreamTimeout) if err != nil { return nil, errors.Wrap(err, "cannot create new RPC connection") } - defer rpcConn.Close() - - tsClient := tunnelpogs.TunnelServer_PogsClient{Client: rpcConn.Bootstrap(ctx)} - + defer tsClient.Close() return tsClient.Connect(ctx, parameters) } func (c *Connection) Shutdown() { c.muxer.Shutdown() } - -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(logger.WithField("rpc", "connect"), rpc.StreamTransport(stream)), - tunnelrpc.ConnLog(logger.WithField("rpc", "connect")), - ), nil -} diff --git a/connection/manager.go b/connection/manager.go index ee4b4870..9ffc50ec 100644 --- a/connection/manager.go +++ b/connection/manager.go @@ -7,15 +7,15 @@ import ( "sync" "time" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/sirupsen/logrus" + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/streamhandler" - "github.com/cloudflare/cloudflared/tunnelrpc/pogs" - "github.com/prometheus/client_golang/prometheus" - - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" + tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( @@ -58,12 +58,12 @@ func newMetrics(namespace, subsystem string) *metrics { // EdgeManagerConfigurable is the configurable attributes of a EdgeConnectionManager type EdgeManagerConfigurable struct { TunnelHostnames []h2mux.TunnelHostname - *pogs.EdgeConnectionConfig + *tunnelpogs.EdgeConnectionConfig } type CloudflaredConfig struct { CloudflaredID uuid.UUID - Tags []pogs.Tag + Tags []tunnelpogs.Tag BuildInfo *buildinfo.BuildInfo IntentLabel string } @@ -126,7 +126,7 @@ func (em *EdgeManager) UpdateConfigurable(newConfigurable *EdgeManagerConfigurab em.state.updateConfigurable(newConfigurable) } -func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError { +func (em *EdgeManager) newConnection(ctx context.Context) *tunnelpogs.ConnectError { edgeTCPAddr := em.serviceDiscoverer.Addr() configurable := em.state.getConfigurable() edgeConn, err := DialEdge(ctx, configurable.Timeout, em.tlsConfig, edgeTCPAddr) @@ -154,7 +154,7 @@ func (em *EdgeManager) newConnection(ctx context.Context) *pogs.ConnectError { go em.serveConn(ctx, h2muxConn) - connResult, err := h2muxConn.Connect(ctx, &pogs.ConnectParameters{ + connResult, err := h2muxConn.Connect(ctx, &tunnelpogs.ConnectParameters{ CloudflaredID: em.cloudflaredConfig.CloudflaredID, CloudflaredVersion: em.cloudflaredConfig.BuildInfo.CloudflaredVersion, NumPreviousAttempts: 0, @@ -285,8 +285,8 @@ func (ems *edgeManagerState) getUserCredential() []byte { return ems.userCredential } -func retryConnection(cause string) *pogs.ConnectError { - return &pogs.ConnectError{ +func retryConnection(cause string) *tunnelpogs.ConnectError { + return &tunnelpogs.ConnectError{ Cause: cause, RetryAfter: defaultRetryAfter, ShouldRetry: true, diff --git a/connection/manager_test.go b/connection/manager_test.go index 6732ce24..a8dd0e58 100644 --- a/connection/manager_test.go +++ b/connection/manager_test.go @@ -4,15 +4,15 @@ import ( "testing" "time" - "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" + "github.com/google/uuid" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" + + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/streamhandler" "github.com/cloudflare/cloudflared/tunnelrpc/pogs" - - "github.com/google/uuid" - "github.com/sirupsen/logrus" ) var ( diff --git a/connection/rpc.go b/connection/rpc.go new file mode 100644 index 00000000..9c10c334 --- /dev/null +++ b/connection/rpc.go @@ -0,0 +1,49 @@ +package connection + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + rpc "zombiezen.com/go/capnproto2/rpc" + + "github.com/cloudflare/cloudflared/h2mux" + "github.com/cloudflare/cloudflared/tunnelrpc" + tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" +) + +// NewRPCClient creates and returns a new RPC client, which will communicate +// using a stream on the given muxer +func NewRPCClient( + ctx context.Context, + muxer *h2mux.Muxer, + logger *logrus.Entry, + openStreamTimeout time.Duration, +) (client tunnelpogs.TunnelServer_PogsClient, err error) { + openStreamCtx, openStreamCancel := context.WithTimeout(ctx, openStreamTimeout) + defer openStreamCancel() + stream, err := muxer.OpenRPCStream(openStreamCtx) + if err != nil { + return + } + + if !isRPCStreamResponse(stream.Headers) { + stream.Close() + err = fmt.Errorf("rpc: bad response headers: %v", stream.Headers) + return + } + + conn := rpc.NewConn( + tunnelrpc.NewTransportLogger(logger, rpc.StreamTransport(stream)), + tunnelrpc.ConnLog(logger), + ) + client = tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn} + return client, nil +} + +func isRPCStreamResponse(headers []h2mux.Header) bool { + return len(headers) == 1 && + headers[0].Name == ":status" && + headers[0].Value == "200" +} diff --git a/origin/supervisor.go b/origin/supervisor.go index 6c58674f..cea9f9b3 100644 --- a/origin/supervisor.go +++ b/origin/supervisor.go @@ -6,12 +6,11 @@ import ( "net" "time" + "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/signal" - - "github.com/google/uuid" ) const ( diff --git a/origin/tunnel.go b/origin/tunnel.go index b6c2ea68..4c931c10 100644 --- a/origin/tunnel.go +++ b/origin/tunnel.go @@ -14,23 +14,21 @@ import ( "sync" "time" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + log "github.com/sirupsen/logrus" + "golang.org/x/sync/errgroup" + "github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo" "github.com/cloudflare/cloudflared/connection" "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/signal" "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/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/websocket" - - "github.com/google/uuid" - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - log "github.com/sirupsen/logrus" - "golang.org/x/sync/errgroup" - rpc "zombiezen.com/go/capnproto2/rpc" ) const ( @@ -288,16 +286,6 @@ func ServeTunnel( return nil, true } -func IsRPCStreamResponse(headers []h2mux.Header) bool { - if len(headers) != 1 { - return false - } - if headers[0].Name != ":status" || headers[0].Value != "200" { - return false - } - return true -} - func RegisterTunnel( ctx context.Context, muxer *h2mux.Muxer, @@ -308,28 +296,18 @@ func RegisterTunnel( uuid uuid.UUID, ) error { config.TransportLogger.Debug("initiating RPC stream to register") - stream, err := openStream(ctx, muxer) + tunnelServer, err := connection.NewRPCClient(ctx, muxer, config.TransportLogger.WithField("subsystem", "rpc-register"), openStreamTimeout) if err != nil { // RPC stream open error return newClientRegisterTunnelError(err, config.Metrics.rpcFail) } - if !IsRPCStreamResponse(stream.Headers) { - // stream response error - return newClientRegisterTunnelError(err, config.Metrics.rpcFail) - } - conn := rpc.NewConn( - tunnelrpc.NewTransportLogger(config.TransportLogger.WithField("subsystem", "rpc-register"), rpc.StreamTransport(stream)), - tunnelrpc.ConnLog(config.TransportLogger.WithField("subsystem", "rpc-transport")), - ) - defer conn.Close() - ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)} + defer tunnelServer.Close() // Request server info without blocking tunnel registration; must use capnp library directly. - tsClient := tunnelrpc.TunnelServer{Client: ts.Client} - serverInfoPromise := tsClient.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error { + serverInfoPromise := tunnelrpc.TunnelServer{Client: tunnelServer.Client}.GetServerInfo(ctx, func(tunnelrpc.TunnelServer_getServerInfo_Params) error { return nil }) LogServerInfo(serverInfoPromise.Result(), connectionID, config.Metrics, logger) - registration := ts.RegisterTunnel( + registration := tunnelServer.RegisterTunnel( ctx, config.OriginCert, config.Hostname, @@ -369,7 +347,7 @@ func RegisterTunnel( return nil } -func processRegisterTunnelError(err pogs.TunnelRegistrationError, metrics *TunnelMetrics) error { +func processRegisterTunnelError(err tunnelpogs.TunnelRegistrationError, metrics *TunnelMetrics) error { if err.Error() == DuplicateConnectionError { metrics.regFail.WithLabelValues("dup_edge_conn").Inc() return dupConnRegisterTunnelError{} @@ -384,35 +362,15 @@ func processRegisterTunnelError(err pogs.TunnelRegistrationError, metrics *Tunne func UnregisterTunnel(muxer *h2mux.Muxer, gracePeriod time.Duration, logger *log.Logger) error { logger.Debug("initiating RPC stream to unregister") ctx := context.Background() - stream, err := openStream(ctx, muxer) + ts, err := connection.NewRPCClient(ctx, muxer, logger.WithField("subsystem", "rpc-unregister"), openStreamTimeout) if err != nil { // RPC stream open error return err } - if !IsRPCStreamResponse(stream.Headers) { - // stream response error - return err - } - conn := rpc.NewConn( - tunnelrpc.NewTransportLogger(logger.WithField("subsystem", "rpc-unregister"), rpc.StreamTransport(stream)), - tunnelrpc.ConnLog(logger.WithField("subsystem", "rpc-transport")), - ) - defer conn.Close() - ts := tunnelpogs.TunnelServer_PogsClient{Client: conn.Bootstrap(ctx)} // gracePeriod is encoded in int64 using capnproto return ts.UnregisterTunnel(ctx, gracePeriod.Nanoseconds()) } -func openStream(ctx context.Context, muxer *h2mux.Muxer) (*h2mux.MuxedStream, error) { - openStreamCtx, cancel := context.WithTimeout(ctx, openStreamTimeout) - defer cancel() - return muxer.OpenStream(openStreamCtx, []h2mux.Header{ - {Name: ":method", Value: "RPC"}, - {Name: ":scheme", Value: "capnp"}, - {Name: ":path", Value: "*"}, - }, nil) -} - func LogServerInfo( promise tunnelrpc.ServerInfo_Promise, connectionID uint8, From b499c0fdbad0b3cfb3035b4cacba423804909673 Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Tue, 3 Dec 2019 15:01:28 -0600 Subject: [PATCH 20/21] TUN-2608: h2mux.Muxer.Shutdown always returns a non-nil channel --- h2mux/activestreammap.go | 46 +++++++----- h2mux/activestreammap_test.go | 134 ++++++++++++++++++++++++++++++++++ h2mux/h2mux.go | 6 +- h2mux/h2mux_test.go | 8 ++ h2mux/muxedstream.go | 12 ++- h2mux/muxreader.go | 11 ++- 6 files changed, 188 insertions(+), 29 deletions(-) create mode 100644 h2mux/activestreammap_test.go diff --git a/h2mux/activestreammap.go b/h2mux/activestreammap.go index a15bee89..1138bea4 100644 --- a/h2mux/activestreammap.go +++ b/h2mux/activestreammap.go @@ -13,26 +13,28 @@ type activeStreamMap struct { sync.RWMutex // streams tracks open streams. streams map[uint32]*MuxedStream - // streamsEmpty is a chan that should be closed when no more streams are open. - streamsEmpty chan struct{} // nextStreamID is the next ID to use on our side of the connection. // This is odd for clients, even for servers. nextStreamID uint32 // maxPeerStreamID is the ID of the most recent stream opened by the peer. maxPeerStreamID uint32 + // activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams + activeStreams prometheus.Gauge + // ignoreNewStreams is true when the connection is being shut down. New streams // cannot be registered. ignoreNewStreams bool - // activeStreams is a gauge shared by all muxers of this process to expose the total number of active streams - activeStreams prometheus.Gauge + // streamsEmpty is a chan that will be closed when no more streams are open. + streamsEmptyChan chan struct{} + closeOnce sync.Once } func newActiveStreamMap(useClientStreamNumbers bool, activeStreams prometheus.Gauge) *activeStreamMap { m := &activeStreamMap{ - streams: make(map[uint32]*MuxedStream), - streamsEmpty: make(chan struct{}), - nextStreamID: 1, - activeStreams: activeStreams, + streams: make(map[uint32]*MuxedStream), + streamsEmptyChan: make(chan struct{}), + nextStreamID: 1, + activeStreams: activeStreams, } // Client initiated stream uses odd stream ID, server initiated stream uses even stream ID if !useClientStreamNumbers { @@ -41,6 +43,12 @@ func newActiveStreamMap(useClientStreamNumbers bool, activeStreams prometheus.Ga return m } +func (m *activeStreamMap) notifyStreamsEmpty() { + m.closeOnce.Do(func() { + close(m.streamsEmptyChan) + }) +} + // Len returns the number of active streams. func (m *activeStreamMap) Len() int { m.RLock() @@ -79,30 +87,27 @@ func (m *activeStreamMap) Delete(streamID uint32) { delete(m.streams, streamID) m.activeStreams.Dec() } - if len(m.streams) == 0 && m.streamsEmpty != nil { - close(m.streamsEmpty) - m.streamsEmpty = nil + if len(m.streams) == 0 { + m.notifyStreamsEmpty() } } -// Shutdown blocks new streams from being created. It returns a channel that receives an event -// once the last stream has closed, or nil if a shutdown is in progress. -func (m *activeStreamMap) Shutdown() <-chan struct{} { +// Shutdown blocks new streams from being created. +// It returns `done`, a channel that is closed once the last stream has closed +// and `progress`, whether a shutdown was already in progress +func (m *activeStreamMap) Shutdown() (done <-chan struct{}, alreadyInProgress bool) { m.Lock() defer m.Unlock() if m.ignoreNewStreams { // already shutting down - return nil + return m.streamsEmptyChan, true } m.ignoreNewStreams = true - done := make(chan struct{}) if len(m.streams) == 0 { // nothing to shut down - close(done) - return done + m.notifyStreamsEmpty() } - m.streamsEmpty = done - return done + return m.streamsEmptyChan, false } // AcquireLocalID acquires a new stream ID for a stream you're opening. @@ -170,4 +175,5 @@ func (m *activeStreamMap) Abort() { stream.Close() } m.ignoreNewStreams = true + m.notifyStreamsEmpty() } diff --git a/h2mux/activestreammap_test.go b/h2mux/activestreammap_test.go new file mode 100644 index 00000000..5f7cd2cc --- /dev/null +++ b/h2mux/activestreammap_test.go @@ -0,0 +1,134 @@ +package h2mux + +import ( + "sync" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShutdown(t *testing.T) { + const numStreams = 1000 + m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name())) + + // Add all the streams + { + var wg sync.WaitGroup + wg.Add(numStreams) + for i := 0; i < numStreams; i++ { + go func(streamID int) { + defer wg.Done() + stream := &MuxedStream{streamID: uint32(streamID)} + ok := m.Set(stream) + assert.True(t, ok) + }(i) + } + wg.Wait() + } + assert.Equal(t, numStreams, m.Len(), "All the streams should have been added") + + shutdownChan, alreadyInProgress := m.Shutdown() + select { + case <-shutdownChan: + assert.Fail(t, "before Shutdown(), shutdownChan shouldn't be closed") + default: + } + assert.False(t, alreadyInProgress) + + shutdownChan2, alreadyInProgress2 := m.Shutdown() + assert.Equal(t, shutdownChan, shutdownChan2, "repeated calls to Shutdown() should return the same channel") + assert.True(t, alreadyInProgress2, "repeated calls to Shutdown() should return true for 'in progress'") + + // Delete all the streams + { + var wg sync.WaitGroup + wg.Add(numStreams) + for i := 0; i < numStreams; i++ { + go func(streamID int) { + defer wg.Done() + m.Delete(uint32(streamID)) + }(i) + } + wg.Wait() + } + assert.Equal(t, 0, m.Len(), "All the streams should have been deleted") + + select { + case <-shutdownChan: + default: + assert.Fail(t, "After all the streams are deleted, shutdownChan should have been closed") + } +} + +type noopBuffer struct { + isClosed bool +} + +func (t *noopBuffer) Read(p []byte) (n int, err error) { return len(p), nil } +func (t *noopBuffer) Write(p []byte) (n int, err error) { return len(p), nil } +func (t *noopBuffer) Reset() {} +func (t *noopBuffer) Len() int { return 0 } +func (t *noopBuffer) Close() error { t.isClosed = true; return nil } +func (t *noopBuffer) Closed() bool { return t.isClosed } + +type noopReadyList struct{} + +func (_ *noopReadyList) Signal(streamID uint32) {} + +func TestAbort(t *testing.T) { + const numStreams = 1000 + m := newActiveStreamMap(true, NewActiveStreamsMetrics("test", t.Name())) + + var openedStreams sync.Map + + // Add all the streams + { + var wg sync.WaitGroup + wg.Add(numStreams) + for i := 0; i < numStreams; i++ { + go func(streamID int) { + defer wg.Done() + stream := &MuxedStream{ + streamID: uint32(streamID), + readBuffer: &noopBuffer{}, + writeBuffer: &noopBuffer{}, + readyList: &noopReadyList{}, + } + ok := m.Set(stream) + assert.True(t, ok) + + openedStreams.Store(stream.streamID, stream) + }(i) + } + wg.Wait() + } + assert.Equal(t, numStreams, m.Len(), "All the streams should have been added") + + shutdownChan, alreadyInProgress := m.Shutdown() + select { + case <-shutdownChan: + assert.Fail(t, "before Abort(), shutdownChan shouldn't be closed") + default: + } + assert.False(t, alreadyInProgress) + + m.Abort() + assert.Equal(t, numStreams, m.Len(), "Abort() shouldn't delete any streams") + openedStreams.Range(func(key interface{}, value interface{}) bool { + stream := value.(*MuxedStream) + readBuffer := stream.readBuffer.(*noopBuffer) + writeBuffer := stream.writeBuffer.(*noopBuffer) + return assert.True(t, readBuffer.isClosed && writeBuffer.isClosed, "Abort() should have closed all the streams") + }) + + select { + case <-shutdownChan: + default: + assert.Fail(t, "after Abort(), shutdownChan should have been closed") + } + + // multiple aborts shouldn't cause any issues + m.Abort() + m.Abort() + m.Abort() +} diff --git a/h2mux/h2mux.go b/h2mux/h2mux.go index 59f722cc..8a3330d3 100644 --- a/h2mux/h2mux.go +++ b/h2mux/h2mux.go @@ -353,9 +353,11 @@ func (m *Muxer) Serve(ctx context.Context) error { } // Shutdown is called to initiate the "happy path" of muxer termination. -func (m *Muxer) Shutdown() { +// It blocks new streams from being created. +// It returns a channel that is closed when the last stream has been closed. +func (m *Muxer) Shutdown() <-chan struct{} { m.explicitShutdown.Fuse(true) - m.muxReader.Shutdown() + return m.muxReader.Shutdown() } // IsUnexpectedTunnelError identifies errors that are expected when shutting down the h2mux tunnel. diff --git a/h2mux/h2mux_test.go b/h2mux/h2mux_test.go index 9b9ce13c..b7995232 100644 --- a/h2mux/h2mux_test.go +++ b/h2mux/h2mux_test.go @@ -55,6 +55,8 @@ func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc) DefaultWindowSize: (1 << 8) - 1, MaxWindowSize: (1 << 15) - 1, StreamWriteBufferMaxLen: 1024, + HeartbeatInterval: defaultTimeout, + MaxHeartbeats: defaultRetries, }, OriginConn: origin, EdgeMuxConfig: MuxerConfig{ @@ -65,6 +67,8 @@ func NewDefaultMuxerPair(t assert.TestingT, testName string, f MuxedStreamFunc) DefaultWindowSize: (1 << 8) - 1, MaxWindowSize: (1 << 15) - 1, StreamWriteBufferMaxLen: 1024, + HeartbeatInterval: defaultTimeout, + MaxHeartbeats: defaultRetries, }, EdgeConn: edge, doneC: make(chan struct{}), @@ -83,6 +87,8 @@ func NewCompressedMuxerPair(t assert.TestingT, testName string, quality Compress Name: "origin", CompressionQuality: quality, Logger: log.NewEntry(log.New()), + HeartbeatInterval: defaultTimeout, + MaxHeartbeats: defaultRetries, }, OriginConn: origin, EdgeMuxConfig: MuxerConfig{ @@ -91,6 +97,8 @@ func NewCompressedMuxerPair(t assert.TestingT, testName string, quality Compress Name: "edge", CompressionQuality: quality, Logger: log.NewEntry(log.New()), + HeartbeatInterval: defaultTimeout, + MaxHeartbeats: defaultRetries, }, EdgeConn: edge, doneC: make(chan struct{}), diff --git a/h2mux/muxedstream.go b/h2mux/muxedstream.go index 6bafa19d..a37270cc 100644 --- a/h2mux/muxedstream.go +++ b/h2mux/muxedstream.go @@ -17,6 +17,12 @@ type ReadWriteClosedCloser interface { Closed() bool } +// MuxedStreamDataSignaller is a write-only *ReadyList +type MuxedStreamDataSignaller interface { + // Non-blocking: call this when data is ready to be sent for the given stream ID. + Signal(ID uint32) +} + // MuxedStream is logically an HTTP/2 stream, with an additional buffer for outgoing data. type MuxedStream struct { streamID uint32 @@ -55,8 +61,8 @@ type MuxedStream struct { // This is the amount of bytes that are in the peer's receive window // (how much data we can send from this stream). sendWindow uint32 - // Reference to the muxer's readyList; signal this for stream data to be sent. - readyList *ReadyList + // The muxer's readyList + readyList MuxedStreamDataSignaller // The headers that should be sent, and a flag so we only send them once. headersSent bool writeHeaders []Header @@ -88,7 +94,7 @@ func (th TunnelHostname) IsSet() bool { return th != "" } -func NewStream(config MuxerConfig, writeHeaders []Header, readyList *ReadyList, dictionaries h2Dictionaries) *MuxedStream { +func NewStream(config MuxerConfig, writeHeaders []Header, readyList MuxedStreamDataSignaller, dictionaries h2Dictionaries) *MuxedStream { return &MuxedStream{ responseHeadersReceived: make(chan struct{}), readBuffer: NewSharedBuffer(), diff --git a/h2mux/muxreader.go b/h2mux/muxreader.go index 728c94c4..c9b4dff7 100644 --- a/h2mux/muxreader.go +++ b/h2mux/muxreader.go @@ -51,10 +51,12 @@ type MuxReader struct { dictionaries h2Dictionaries } -func (r *MuxReader) Shutdown() { - done := r.streams.Shutdown() - if done == nil { - return +// Shutdown blocks new streams from being created. +// It returns a channel that is closed once the last stream has closed. +func (r *MuxReader) Shutdown() <-chan struct{} { + done, alreadyInProgress := r.streams.Shutdown() + if alreadyInProgress { + return done } r.sendGoAway(http2.ErrCodeNo) go func() { @@ -62,6 +64,7 @@ func (r *MuxReader) Shutdown() { <-done r.r.Close() }() + return done } func (r *MuxReader) run(parentLogger *log.Entry) error { From 5e7ca1441207bfc26eac8ef31e1c39a2614e276a Mon Sep 17 00:00:00 2001 From: Nick Vollmar Date: Wed, 4 Dec 2019 11:22:08 -0600 Subject: [PATCH 21/21] TUN-2555: origin/supervisor.go calls Authenticate --- cmd/cloudflared/tunnel/cmd.go | 9 +- cmd/cloudflared/tunnel/configuration.go | 1 + origin/backoffhandler.go | 5 + origin/supervisor.go | 178 ++++++++++++++++++++++-- origin/supervisor_test.go | 128 +++++++++++++++++ origin/tunnel.go | 28 ++-- tunnelrpc/pogs/auth_outcome.go | 32 +++-- 7 files changed, 344 insertions(+), 37 deletions(-) create mode 100644 origin/supervisor_test.go diff --git a/cmd/cloudflared/tunnel/cmd.go b/cmd/cloudflared/tunnel/cmd.go index 176a9889..62c6c1c4 100644 --- a/cmd/cloudflared/tunnel/cmd.go +++ b/cmd/cloudflared/tunnel/cmd.go @@ -977,6 +977,12 @@ func tunnelFlags(shouldHide bool) []cli.Flag { EnvVars: []string{"TUNNEL_INTENT"}, Hidden: true, }), + altsrc.NewBoolFlag(&cli.BoolFlag{ + Name: "use-reconnect-token", + Usage: "Test reestablishing connections with the new 'reconnect token' flow.", + EnvVars: []string{"TUNNEL_USE_RECONNECT_TOKEN"}, + Hidden: true, + }), altsrc.NewDurationFlag(&cli.DurationFlag{ Name: "dial-edge-timeout", Usage: "Maximum wait time to set up a connection with the edge", @@ -1044,7 +1050,6 @@ func tunnelFlags(shouldHide bool) []cli.Flag { Usage: "Absolute path of directory to save SSH host keys in", EnvVars: []string{"HOST_KEY_PATH"}, Hidden: true, - }), } -} \ No newline at end of file +} diff --git a/cmd/cloudflared/tunnel/configuration.go b/cmd/cloudflared/tunnel/configuration.go index 8986c9f9..ef24751b 100644 --- a/cmd/cloudflared/tunnel/configuration.go +++ b/cmd/cloudflared/tunnel/configuration.go @@ -275,6 +275,7 @@ func prepareTunnelConfig( TlsConfig: toEdgeTLSConfig, TransportLogger: transportLogger, UseDeclarativeTunnel: c.Bool("use-declarative-tunnels"), + UseReconnectToken: c.Bool("use-reconnect-token"), }, nil } diff --git a/origin/backoffhandler.go b/origin/backoffhandler.go index 97bb9ad8..8ff9752b 100644 --- a/origin/backoffhandler.go +++ b/origin/backoffhandler.go @@ -92,3 +92,8 @@ func (b BackoffHandler) GetBaseTime() time.Duration { } return b.BaseTime } + +// Retries returns the number of retries consumed so far. +func (b *BackoffHandler) Retries() int { + return int(b.retries) +} diff --git a/origin/supervisor.go b/origin/supervisor.go index cea9f9b3..8bb8d046 100644 --- a/origin/supervisor.go +++ b/origin/supervisor.go @@ -2,15 +2,20 @@ package origin import ( "context" + "errors" "fmt" + "math/rand" "net" + "sync" "time" "github.com/google/uuid" "github.com/sirupsen/logrus" "github.com/cloudflare/cloudflared/connection" + "github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/signal" + tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" ) const ( @@ -20,11 +25,23 @@ const ( resolveTTL = time.Hour // Interval between registering new tunnels registrationInterval = time.Second + + subsystemRefreshAuth = "refresh_auth" + // Maximum exponent for 'Authenticate' exponential backoff + refreshAuthMaxBackoff = 10 + // Waiting time before retrying a failed 'Authenticate' connection + refreshAuthRetryDuration = time.Second * 10 +) + +var ( + errJWTUnset = errors.New("JWT unset") + errEventDigestUnset = errors.New("event digest unset") ) type Supervisor struct { - config *TunnelConfig - edgeIPs []*net.TCPAddr + cloudflaredUUID uuid.UUID + config *TunnelConfig + edgeIPs []*net.TCPAddr // nextUnusedEdgeIP is the index of the next addr k edgeIPs to try nextUnusedEdgeIP int lastResolve time.Time @@ -37,6 +54,12 @@ type Supervisor struct { nextConnectedSignal chan struct{} logger *logrus.Entry + + jwtLock *sync.RWMutex + jwt []byte + + eventDigestLock *sync.RWMutex + eventDigest []byte } type resolveResult struct { @@ -49,18 +72,21 @@ type tunnelError struct { err error } -func NewSupervisor(config *TunnelConfig) *Supervisor { +func NewSupervisor(config *TunnelConfig, u uuid.UUID) *Supervisor { return &Supervisor{ + cloudflaredUUID: u, config: config, tunnelErrors: make(chan tunnelError), tunnelsConnecting: map[int]chan struct{}{}, logger: config.Logger.WithField("subsystem", "supervisor"), + jwtLock: &sync.RWMutex{}, + eventDigestLock: &sync.RWMutex{}, } } -func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) error { +func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal) error { logger := s.config.Logger - if err := s.initialize(ctx, connectedSignal, u); err != nil { + if err := s.initialize(ctx, connectedSignal); err != nil { return err } var tunnelsWaiting []int @@ -68,6 +94,12 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u var backoffTimer <-chan time.Time tunnelsActive := s.config.HAConnections + refreshAuthBackoff := &BackoffHandler{MaxRetries: refreshAuthMaxBackoff, BaseTime: refreshAuthRetryDuration, RetryForever: true} + var refreshAuthBackoffTimer <-chan time.Time + if s.config.UseReconnectToken { + refreshAuthBackoffTimer = time.After(refreshAuthRetryDuration) + } + for { select { // Context cancelled @@ -103,10 +135,20 @@ func (s *Supervisor) Run(ctx context.Context, connectedSignal *signal.Signal, u case <-backoffTimer: backoffTimer = nil for _, index := range tunnelsWaiting { - go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index), u) + go s.startTunnel(ctx, index, s.newConnectedTunnelSignal(index)) } tunnelsActive += len(tunnelsWaiting) tunnelsWaiting = nil + // Time to call Authenticate + case <-refreshAuthBackoffTimer: + newTimer, err := s.refreshAuth(ctx, refreshAuthBackoff, s.authenticate) + if err != nil { + logger.WithError(err).Error("Authentication failed") + // Permanent failure. Leave the `select` without setting the + // channel to be non-null, so we'll never hit this case of the `select` again. + continue + } + refreshAuthBackoffTimer = newTimer // Tunnel successfully connected case <-s.nextConnectedSignal: if !s.waitForNextTunnel(s.nextConnectedIndex) && len(tunnelsWaiting) == 0 { @@ -127,7 +169,7 @@ 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 { +func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Signal) error { logger := s.logger edgeIPs, err := s.resolveEdgeIPs() @@ -144,12 +186,12 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig s.lastResolve = time.Now() // check entitlement and version too old error before attempting to register more tunnels s.nextUnusedEdgeIP = s.config.HAConnections - go s.startFirstTunnel(ctx, connectedSignal, u) + go s.startFirstTunnel(ctx, connectedSignal) select { case <-ctx.Done(): <-s.tunnelErrors // Error can't be nil. A nil error signals that initialization succeed - return fmt.Errorf("context was canceled") + return ctx.Err() case tunnelError := <-s.tunnelErrors: return tunnelError.err case <-connectedSignal.Wait(): @@ -157,7 +199,7 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig // At least one successful connection, so start the rest for i := 1; i < s.config.HAConnections; i++ { ch := signal.New(make(chan struct{})) - go s.startTunnel(ctx, i, ch, u) + go s.startTunnel(ctx, i, ch) time.Sleep(registrationInterval) } return nil @@ -165,8 +207,8 @@ func (s *Supervisor) initialize(ctx context.Context, connectedSignal *signal.Sig // startTunnel starts the first tunnel connection. The resulting error will be sent on // s.tunnelErrors. It will send a signal via connectedSignal if registration succeed -func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal, u uuid.UUID) { - err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u) +func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *signal.Signal) { + err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, s.cloudflaredUUID) defer func() { s.tunnelErrors <- tunnelError{index: 0, err: err} }() @@ -187,14 +229,14 @@ func (s *Supervisor) startFirstTunnel(ctx context.Context, connectedSignal *sign default: return } - err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, u) + err = ServeTunnelLoop(ctx, s.config, s.getEdgeIP(0), 0, connectedSignal, s.cloudflaredUUID) } } // startTunnel starts a new tunnel connection. The resulting error will be sent on // s.tunnelErrors. -func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal, u uuid.UUID) { - err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(index), uint8(index), connectedSignal, u) +func (s *Supervisor) startTunnel(ctx context.Context, index int, connectedSignal *signal.Signal) { + err := ServeTunnelLoop(ctx, s.config, s.getEdgeIP(index), uint8(index), connectedSignal, s.cloudflaredUUID) s.tunnelErrors <- tunnelError{index: index, err: err} } @@ -252,3 +294,109 @@ func (s *Supervisor) replaceEdgeIP(badIPIndex int) { s.edgeIPs[badIPIndex] = s.edgeIPs[s.nextUnusedEdgeIP] s.nextUnusedEdgeIP++ } + +func (s *Supervisor) ReconnectToken() ([]byte, error) { + s.jwtLock.RLock() + defer s.jwtLock.RUnlock() + if s.jwt == nil { + return nil, errJWTUnset + } + return s.jwt, nil +} + +func (s *Supervisor) SetReconnectToken(jwt []byte) { + s.jwtLock.Lock() + defer s.jwtLock.Unlock() + s.jwt = jwt +} + +func (s *Supervisor) EventDigest() ([]byte, error) { + s.eventDigestLock.RLock() + defer s.eventDigestLock.RUnlock() + if s.eventDigest == nil { + return nil, errEventDigestUnset + } + return s.eventDigest, nil +} + +func (s *Supervisor) SetEventDigest(eventDigest []byte) { + s.eventDigestLock.Lock() + defer s.eventDigestLock.Unlock() + s.eventDigest = eventDigest +} + +func (s *Supervisor) refreshAuth( + ctx context.Context, + backoff *BackoffHandler, + authenticate func(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error), +) (retryTimer <-chan time.Time, err error) { + logger := s.config.Logger.WithField("subsystem", subsystemRefreshAuth) + authOutcome, err := authenticate(ctx, backoff.Retries()) + if err != nil { + if duration, ok := backoff.GetBackoffDuration(ctx); ok { + logger.WithError(err).Warnf("Retrying in %v", duration) + return backoff.BackoffTimer(), nil + } + return nil, err + } + // clear backoff timer + backoff.SetGracePeriod() + + switch outcome := authOutcome.(type) { + case tunnelpogs.AuthSuccess: + s.SetReconnectToken(outcome.JWT()) + return timeAfter(outcome.RefreshAfter()), nil + case tunnelpogs.AuthUnknown: + return timeAfter(outcome.RefreshAfter()), nil + case tunnelpogs.AuthFail: + return nil, outcome + default: + return nil, fmt.Errorf("Unexpected outcome type %T", authOutcome) + } +} + +func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int) (tunnelpogs.AuthOutcome, error) { + arbitraryEdgeIP := s.getEdgeIP(rand.Int()) + edgeConn, err := connection.DialEdge(ctx, dialTimeout, s.config.TlsConfig, arbitraryEdgeIP) + if err != nil { + return nil, err + } + defer edgeConn.Close() + + handler := h2mux.MuxedStreamFunc(func(*h2mux.MuxedStream) error { + // This callback is invoked by h2mux when the edge initiates a stream. + return nil // noop + }) + muxerConfig := s.config.muxerConfig(handler) + muxerConfig.Logger = muxerConfig.Logger.WithField("subsystem", subsystemRefreshAuth) + muxer, err := h2mux.Handshake(edgeConn, edgeConn, muxerConfig, s.config.Metrics.activeStreams) + if err != nil { + return nil, err + } + go muxer.Serve(ctx) + defer func() { + // If we don't wait for the muxer shutdown here, edgeConn.Close() runs before the muxer connections are done, + // and the user sees log noise: "error writing data", "connection closed unexpectedly" + <-muxer.Shutdown() + }() + + tunnelServer, err := connection.NewRPCClient(ctx, muxer, s.logger.WithField("subsystem", subsystemRefreshAuth), openStreamTimeout) + if err != nil { + return nil, err + } + defer tunnelServer.Close() + + const arbitraryConnectionID = uint8(0) + registrationOptions := s.config.RegistrationOptions(arbitraryConnectionID, edgeConn.LocalAddr().String(), s.cloudflaredUUID) + registrationOptions.NumPreviousAttempts = uint8(numPreviousAttempts) + authResponse, err := tunnelServer.Authenticate( + ctx, + s.config.OriginCert, + s.config.Hostname, + registrationOptions, + ) + if err != nil { + return nil, err + } + return authResponse.Outcome(), nil +} diff --git a/origin/supervisor_test.go b/origin/supervisor_test.go new file mode 100644 index 00000000..559c0400 --- /dev/null +++ b/origin/supervisor_test.go @@ -0,0 +1,128 @@ +package origin + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/google/uuid" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + + tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" +) + +func TestRefreshAuthBackoff(t *testing.T) { + logger := logrus.New() + logger.Level = logrus.ErrorLevel + + var wait time.Duration + timeAfter = func(d time.Duration) <-chan time.Time { + wait = d + return time.After(d) + } + + s := NewSupervisor(&TunnelConfig{Logger: logger}, uuid.New()) + backoff := &BackoffHandler{MaxRetries: 3} + auth := func(ctx context.Context, n int) (tunnelpogs.AuthOutcome, error) { + return nil, fmt.Errorf("authentication failure") + } + + // authentication failures should consume the backoff + for i := uint(0); i < backoff.MaxRetries; i++ { + retryChan, err := s.refreshAuth(context.Background(), backoff, auth) + assert.NoError(t, err) + assert.NotNil(t, retryChan) + assert.Equal(t, (1< 0 { + if len(ar.Jwt) > 0 { return NewAuthSuccess(ar.Jwt, ar.HoursUntilRefresh) } @@ -57,6 +57,10 @@ func NewAuthSuccess(jwt []byte, hoursUntilRefresh uint8) AuthSuccess { return AuthSuccess{jwt: jwt, hoursUntilRefresh: hoursUntilRefresh} } +func (ao AuthSuccess) JWT() []byte { + return ao.jwt +} + // RefreshAfter is how long cloudflared should wait before rerunning Authenticate. func (ao AuthSuccess) RefreshAfter() time.Duration { return hoursToTime(ao.hoursUntilRefresh) @@ -81,6 +85,10 @@ func NewAuthFail(err error) AuthFail { return AuthFail{err: err} } +func (ao AuthFail) Error() string { + return ao.err.Error() +} + // Serialize into an AuthenticateResponse which can be sent via Capnp func (ao AuthFail) Serialize() AuthenticateResponse { return AuthenticateResponse{ @@ -100,6 +108,10 @@ func NewAuthUnknown(err error, hoursUntilRefresh uint8) AuthUnknown { return AuthUnknown{err: err, hoursUntilRefresh: hoursUntilRefresh} } +func (ao AuthUnknown) Error() string { + return ao.err.Error() +} + // RefreshAfter is how long cloudflared should wait before rerunning Authenticate. func (ao AuthUnknown) RefreshAfter() time.Duration { return hoursToTime(ao.hoursUntilRefresh) @@ -117,4 +129,4 @@ func (ao AuthUnknown) isAuthOutcome() {} func hoursToTime(hours uint8) time.Duration { return time.Duration(hours) * time.Hour -} +} \ No newline at end of file