Merge branch 'cloudflare:master' into master

This commit is contained in:
Jauder Ho 2022-03-01 23:39:22 -08:00 committed by GitHub
commit 7713f175f3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 3732 additions and 1064 deletions

31
.github/ISSUE_TEMPLATE/bug-report---.md vendored Normal file
View File

@ -0,0 +1,31 @@
---
name: "Bug report \U0001F41B"
about: Create a report to help us improve cloudflared
title: ''
labels: awaiting reply, bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Configure '...'
2. Run '....'
3. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Environment and versions**
- OS: [e.g. MacOS]
- Architecture: [e.g. AMD, ARM]
- Version: [e.g. 2022.02.0]
**Logs and errors**
If applicable, add logs or errors to help explain your problem.
**Additional context**
Add any other context about the problem here.

View File

@ -0,0 +1,17 @@
---
name: "Feature request \U0001F4A1"
about: Suggest a feature or enhancement for cloudflared
title: ''
labels: awaiting reply, feature-request
assignees: ''
---
**Describe the feature you'd like**
A clear and concise description of the feature. What problem does it solve for you?
**Describe alternatives you've considered**
Are there any alternatives to solving this problem? If so, what was your experience with them?
**Additional context**
Add any other context or screenshots about the feature request here.

322
LICENSE
View File

@ -1,155 +1,211 @@
SERVICES AGREEMENT Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
Your installation of this software is symbol of your signature indicating that TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
you accept the terms of this Services Agreement (this "Agreement"). This
Agreement is a legal agreement between you (either an individual or a single
entity) and CloudFlare, Inc. for the services being provided to you by
CloudFlare or its authorized representative (the "Services"), including any
computer software and any associated media, printed materials, and "online" or
electronic documentation provided in connection with the Services (the
"Software" and together with the Services are hereinafter collectively referred
to as the "Solution"). If the user is not an individual, then "you" means your
company, its officers, members, employees, agents, representatives, successors
and assigns. BY USING THE SOLUTION, YOU ARE INDICATING THAT YOU HAVE READ, AND
AGREE TO BE BOUND BY, THE POLICIES, TERMS, AND CONDITIONS SET FORTH BELOW IN
THEIR ENTIRETY WITHOUT LIMITATION OR QUALIFICATION, AS WELL AS BY ALL APPLICABLE
LAWS AND REGULATIONS, AS IF YOU HAD HANDWRITTEN YOUR NAME ON A CONTRACT. IF YOU
DO NOT AGREE TO THESE TERMS AND CONDITIONS, YOU MAY NOT USE THE SOLUTION.
1. GRANT OF RIGHTS 1. Definitions.
1.1 Grant of License. The Solution is licensed by CloudFlare and its "License" shall mean the terms and conditions for use, reproduction,
licensors, not sold. Subject to the terms and conditions of this Agreement, and distribution as defined by Sections 1 through 9 of this document.
CloudFlare hereby grants you a nonexclusive, nonsublicensable, nontransferable
license to use the Solution. You may examine source code, if provided to you,
solely for the limited purpose of evaluating the Software for security flaws.
You may also use the Service to create derivative works which are exclusively
compatible with any CloudFlare product serviceand no other product or service.
This license applies to the parts of the Solution developed by CloudFlare. The
Solution may also incorporate externally maintained libraries and other open software.
These resources may be governed by other licenses.
1.2 Restrictions. The license granted herein is granted solely to you and "Licensor" shall mean the copyright owner or entity authorized by
not, by implication or otherwise, to any of your parents, subsidiaries or the copyright owner that is granting the License.
affiliates. No right is granted hereunder to use the Solution to perform
services for third parties. All rights not expressly granted hereunder are
reserved to CloudFlare. You may not use the Solution except as explicitly
permitted under this Agreement. You are expressly prohibited from modifying,
adapting, translating, preparing derivative works from, decompiling, reverse
engineering, disassembling or otherwise attempting to derive source code from
the Software used to provide the Services or any internal data files generated
by the Solution. You are also prohibited from removing, obscuring or altering
any copyright notice, trademarks, or other proprietary rights notices affixed to
or associated with the Solution.
1.3 Ownership. As between the parties, CloudFlare and/or its licensors own "Legal Entity" shall mean the union of the acting entity and all
and shall retain all right, title, and interest in and to the Solution, other entities that control, are controlled by, or are under common
including any and all technology embodied therein, including all copyrights, control with that entity. For the purposes of this definition,
patents, trade secrets, trade dress and other proprietary rights associated "control" means (i) the power, direct or indirect, to cause the
therewith, and any derivative works created there from. direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
2. LIMITATION OF LIABILITY "You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT DOWNLOADING THE SOFTWARE IS AT YOUR "Source" form shall mean the preferred form for making modifications,
SOLE RISK. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND including but not limited to software source code, documentation
AND CLOUDFLARE, ITS LICENSORS AND ITS AUTHORIZED REPRESENTATIVES (TOGETHER FOR source, and configuration files.
PURPOSES HEREOF, "CLOUDFLARE") EXPRESSLY DISCLAIM ALL WARRANTIES, EXPRESS OR
IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. CLOUDFLARE DOES NOT
WARRANT THAT THE FUNCTIONS CONTAINED IN THE SOFTWARE WILL MEET YOUR
REQUIREMENTS, OR THAT THE OPERATION OF THE SOFTWARE WILL BE UNINTERRUPTED OR
ERROR-FREE, OR THAT DEFECTS IN THE SOFTWARE WILL BE CORRECTED. FURTHERMORE,
CLOUDFLARE DOES NOT WARRANT OR MAKE ANY REPRESENTATIONS REGARDING THE SOFTWARE
OR RELATED DOCUMENTATION IN TERMS OF THEIR CORRECTNESS, ACCURACY, RELIABILITY,
OR OTHERWISE. NO ORAL OR WRITTEN INFORMATION OR ADVICE GIVEN BY CLOUDFLARE SHALL
CREATE A WARRANTY OR IN ANY WAY INCREASE THE SCOPE OF THIS WARRANTY.
3. CONFIDENTIALITY "Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
It may be necessary during the set up and performance of the Solution for the "Work" shall mean the work of authorship, whether in Source or
parties to exchange Confidential Information. "Confidential Information" means Object form, made available under the License, as indicated by a
any information whether oral, or written, of a private, secret, proprietary or copyright notice that is included in or attached to the work
confidential nature, concerning either party or its business operations, (an example is provided in the Appendix below).
including without limitation: (a) your data and (b) CloudFlare's access control
systems, specialized network equipment and techniques related to the Solution,
use policies, which include trade secrets of CloudFlare and its licensors. Each
party agrees to use the same degree of care to protect the confidentiality of
the Confidential Information of the other party and to prevent its unauthorized
use or dissemination as it uses to protect its own Confidential Information of a
similar nature, but in no event shall exercise less than due diligence and
reasonable care. Each party agrees to use the Confidential Information of the
other party only for purposes related to the performance of this Agreement. All
Confidential Information remains the property of the party disclosing the
information and no license or other rights to Confidential Information is
granted or implied hereby.
4. TERM AND TERMINATION "Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
4.1 Term. This Agreement shall be effective upon download or install of the "Contribution" shall mean any work of authorship, including
Software. the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
4.2 Termination. This Agreement may be terminated by CloudFlare or its "Contributor" shall mean Licensor and any individual or Legal Entity
authorized representative by written notice to you if any of the following on behalf of whom a Contribution has been received by Licensor and
events occur: (i) you fail to pay any amounts due for the Services and the subsequently incorporated within the Work.
Solution when due and after written notice of such nonpayment has been given to
you; (ii) you are in material breach of any term, condition, or provision of
this Agreement or any other agreement executed by you with CloudFlare or its
authorized representative in connection with the provision of the Solution and
Services (a "Related Agreement"); or (iii) you terminate or suspend your
business, becomes subject to any bankruptcy or insolvency proceeding under
federal or state statutes, or become insolvent or subject to direct control by a
trustee, receiver or similar authority.
4.3 Effect of Termination. Upon the termination of this Agreement for any 2. Grant of Copyright License. Subject to the terms and conditions of
reason: (1) all license rights granted hereunder shall terminate and (2) all this License, each Contributor hereby grants to You a perpetual,
Confidential Information shall be returned to the disclosing party or destroyed. worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
5. MISCELLANEOUS 3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
5.1 Assignment. You may not assign any of your rights or delegate any of 4. Redistribution. You may reproduce and distribute copies of the
your obligations under this Agreement, whether by operation of law or otherwise, Work or Derivative Works thereof in any medium, with or without
without the prior express written consent of CloudFlare or its authorized modifications, and in Source or Object form, provided that You
representative. Any such assignment without the prior express written consent meet the following conditions:
of CloudFlare or its authorized representative shall be void. Subject to the
foregoing, this Agreement will bind and inure to the benefit of the parties,
their respective successors and permitted assigns.
5.2 Waiver and Amendment. No modification, amendment or waiver of any (a) You must give any other recipients of the Work or
provision of this Agreement shall be effective unless in writing and signed by Derivative Works a copy of this License; and
the party to be charged. No failure or delay by either party in exercising any
right, power, or remedy under this Agreement, except as specifically provided
herein, shall operate as a waiver of any such right, power or remedy. Without
limiting the foregoing, terms and conditions on any purchase orders or similar
materials submitted by you to CloudFlare or its authorized representative shall
be of no force or effect.
5.3 Governing Law. This Agreement shall be governed by the laws of the State (b) You must cause any modified files to carry prominent notices
of California, USA, excluding conflict of laws and provisions, and excluding the stating that You changed the files; and
United Nations Convention on Contracts for the International Sale of Goods.
5.4 Notices. All notices, demands or consents required or permitted under (c) You must retain, in the Source form of any Derivative Works
this Agreement shall be in writing. Notice shall be sent to you at the e-mail that You distribute, all copyright, patent, trademark, and
address provided by you to CloudFlare or its authorized representative in attribution notices from the Source form of the Work,
connection with the Solution. excluding those notices that do not pertain to any part of
the Derivative Works; and
5.5 Independent Contractors. The parties are independent contractors. (d) If the Work includes a "NOTICE" text file as part of its
Neither party shall be deemed to be an employee, agent, partner or legal distribution, then any Derivative Works that You distribute must
representative of the other for any purpose and neither shall have any right, include a readable copy of the attribution notices contained
power or authority to create any obligation or responsibility on behalf of the within such NOTICE file, excluding those notices that do not
other. pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
5.6 Severability. If any provision of this Agreement is held by a court of You may add Your own copyright statement to Your modifications and
competent jurisdiction to be contrary to law, such provision shall be changed may provide additional or different license terms and conditions
and interpreted so as to best accomplish the objectives of the original for use, reproduction, or distribution of Your modifications, or
provision to the fullest extent allowed by law and the remaining provisions of for any such Derivative Works as a whole, provided Your use,
this Agreement shall remain in full force and effect. reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5.7 Force Majeure. CloudFlare shall not be liable to the other party for any 5. Submission of Contributions. Unless You explicitly state otherwise,
failure or delay in performance caused by reasons beyond its reasonable control. any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
5.8 Complete Understanding. This Agreement and the Related Agreement 6. Trademarks. This License does not grant permission to use the trade
constitute the final, complete and exclusive agreement between the parties with names, trademarks, service marks, or product names of the Licensor,
respect to the subject matter hereof, and supersedes all previous written and except as required for reasonable and customary use in describing the
oral agreements and communications related to the subject matter of this origin of the Work and reproducing the content of the NOTICE file.
Agreement. To the extent this Agreement and the Related Agreement conflict,
this Agreement shall control. 7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
## Runtime Library Exception to the Apache 2.0 License: ##
As an exception, if you use this Software to compile your source code and
portions of this Software are embedded into the binary product as a result,
you may redistribute such product without providing attribution as would
otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.

View File

@ -1,3 +1,26 @@
2022.2.2
- 2022-02-22 TUN-5754: Allow ingress validate to take plaintext option
- 2022-02-17 TUN-5678: Cloudflared uses typed tunnel API
2022.2.1
- 2022-02-10 TUN-5184: Handle errors in bidrectional streaming (websocket#Stream) gracefully when 1 side has ended
- 2022-02-14 Update issue templates
- 2022-02-14 Update issue templates
- 2022-02-11 TUN-5768: Update cloudflared license file
- 2022-02-11 TUN-5698: Make ingress rules and warp routing dynamically configurable
- 2022-02-14 TUN-5678: Adapt cloudflared to use new typed APIs
- 2022-02-17 Revert "TUN-5678: Adapt cloudflared to use new typed APIs"
- 2022-02-11 TUN-5697: Listen for UpdateConfiguration RPC in quic transport
- 2022-02-04 TUN-5744: Add a test to make sure cloudflared uses scheme defined in ingress rule, not X-Forwarded-Proto header
- 2022-02-07 TUN-5749: Refactor cloudflared to pave way for reconfigurable ingress - Split origin into supervisor and proxy packages - Create configManager to handle dynamic config
- 2021-10-19 TUN-5184: Make sure outstanding websocket write is finished, and no more writes after shutdown
2022.2.0
- 2022-02-02 TUN-4947: Use http when talking to Unix sockets origins
- 2022-02-02 TUN-5695: Define RPC method to update configuration
- 2022-01-27 TUN-5621: Correctly manage QUIC stream closing
- 2022-01-28 TUN-5702: Allow to deserialize config from JSON
2022.1.3 2022.1.3
- 2022-01-21 TUN-5477: Unhide vnet commands - 2022-01-21 TUN-5477: Unhide vnet commands
- 2022-01-24 TUN-5669: Change network command to vnet - 2022-01-24 TUN-5669: Change network command to vnet

View File

@ -48,7 +48,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
if strings.HasSuffix(baseURL, "/") { if strings.HasSuffix(baseURL, "/") {
baseURL = baseURL[:len(baseURL)-1] baseURL = baseURL[:len(baseURL)-1]
} }
accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/tunnels", baseURL, accountTag)) accountLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/cfd_tunnel", baseURL, accountTag))
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to create account level endpoint") return nil, errors.Wrap(err, "failed to create account level endpoint")
} }

View File

@ -5,7 +5,7 @@ import (
) )
type TunnelClient interface { type TunnelClient interface {
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
DeleteTunnel(tunnelID uuid.UUID) error DeleteTunnel(tunnelID uuid.UUID) error
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)

View File

@ -23,6 +23,11 @@ type Tunnel struct {
Connections []Connection `json:"connections"` Connections []Connection `json:"connections"`
} }
type TunnelWithToken struct {
Tunnel
Token string `json:"token"`
}
type Connection struct { type Connection struct {
ColoName string `json:"colo_name"` ColoName string `json:"colo_name"`
ID uuid.UUID `json:"id"` ID uuid.UUID `json:"id"`
@ -63,7 +68,7 @@ func (cp CleanupParams) encode() string {
return cp.queryParams.Encode() return cp.queryParams.Encode()
} }
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error) { func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) {
if name == "" { if name == "" {
return nil, errors.New("tunnel name required") return nil, errors.New("tunnel name required")
} }
@ -83,7 +88,11 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
switch resp.StatusCode { switch resp.StatusCode {
case http.StatusOK: case http.StatusOK:
return unmarshalTunnel(resp.Body) var tunnel TunnelWithToken
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
return nil, serdeErr
}
return &tunnel, nil
case http.StatusConflict: case http.StatusConflict:
return nil, ErrTunnelNameConflict return nil, ErrTunnelNameConflict
} }

View File

@ -31,8 +31,9 @@ import (
"github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/logger" "github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/metrics" "github.com/cloudflare/cloudflared/metrics"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/orchestration"
"github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/signal"
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
"github.com/cloudflare/cloudflared/tunneldns" "github.com/cloudflare/cloudflared/tunneldns"
) )
@ -223,7 +224,7 @@ func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
func StartServer( func StartServer(
c *cli.Context, c *cli.Context,
info *cliutil.BuildInfo, info *cliutil.BuildInfo,
namedTunnel *connection.NamedTunnelConfig, namedTunnel *connection.NamedTunnelProperties,
log *zerolog.Logger, log *zerolog.Logger,
isUIEnabled bool, isUIEnabled bool,
) error { ) error {
@ -333,7 +334,7 @@ func StartServer(
observer.SendURL(quickTunnelURL) observer.SendURL(quickTunnelURL)
} }
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, info, log, logTransport, observer, namedTunnel) tunnelConfig, dynamicConfig, err := prepareTunnelConfig(c, info, log, logTransport, observer, namedTunnel)
if err != nil { if err != nil {
log.Err(err).Msg("Couldn't start tunnel") log.Err(err).Msg("Couldn't start tunnel")
return err return err
@ -353,11 +354,12 @@ func StartServer(
errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, log) errC <- metrics.ServeMetrics(metricsListener, ctx.Done(), readinessServer, quickTunnelURL, log)
}() }()
if err := ingressRules.StartOrigins(&wg, log, ctx.Done(), errC); err != nil { orchestrator, err := orchestration.NewOrchestrator(ctx, dynamicConfig, tunnelConfig.Tags, tunnelConfig.Log)
if err != nil {
return err return err
} }
reconnectCh := make(chan origin.ReconnectSignal, 1) reconnectCh := make(chan supervisor.ReconnectSignal, 1)
if c.IsSet("stdin-control") { if c.IsSet("stdin-control") {
log.Info().Msg("Enabling control through stdin") log.Info().Msg("Enabling control through stdin")
go stdinControl(reconnectCh, log) go stdinControl(reconnectCh, log)
@ -369,7 +371,7 @@ func StartServer(
wg.Done() wg.Done()
log.Info().Msg("Tunnel server stopped") log.Info().Msg("Tunnel server stopped")
}() }()
errC <- origin.StartTunnelDaemon(ctx, tunnelConfig, connectedSignal, reconnectCh, graceShutdownC) errC <- supervisor.StartTunnelDaemon(ctx, tunnelConfig, orchestrator, connectedSignal, reconnectCh, graceShutdownC)
}() }()
if isUIEnabled { if isUIEnabled {
@ -377,7 +379,7 @@ func StartServer(
info.Version(), info.Version(),
hostname, hostname,
metricsListener.Addr().String(), metricsListener.Addr().String(),
&ingressRules, dynamicConfig.Ingress,
tunnelConfig.HAConnections, tunnelConfig.HAConnections,
) )
app := tunnelUI.Launch(ctx, log, logTransport) app := tunnelUI.Launch(ctx, log, logTransport)
@ -998,7 +1000,7 @@ func configureProxyDNSFlags(shouldHide bool) []cli.Flag {
} }
} }
func stdinControl(reconnectCh chan origin.ReconnectSignal, log *zerolog.Logger) { func stdinControl(reconnectCh chan supervisor.ReconnectSignal, log *zerolog.Logger) {
for { for {
scanner := bufio.NewScanner(os.Stdin) scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() { for scanner.Scan() {
@ -1009,7 +1011,7 @@ func stdinControl(reconnectCh chan origin.ReconnectSignal, log *zerolog.Logger)
case "": case "":
break break
case "reconnect": case "reconnect":
var reconnect origin.ReconnectSignal var reconnect supervisor.ReconnectSignal
if len(parts) > 1 { if len(parts) > 1 {
var err error var err error
if reconnect.Delay, err = time.ParseDuration(parts[1]); err != nil { if reconnect.Delay, err = time.ParseDuration(parts[1]); err != nil {

View File

@ -23,7 +23,8 @@ import (
"github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/ingress" "github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/origin" "github.com/cloudflare/cloudflared/orchestration"
"github.com/cloudflare/cloudflared/supervisor"
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/validation" "github.com/cloudflare/cloudflared/validation"
@ -87,7 +88,7 @@ func logClientOptions(c *cli.Context, log *zerolog.Logger) {
} }
} }
func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelConfig) bool { func dnsProxyStandAlone(c *cli.Context, namedTunnel *connection.NamedTunnelProperties) bool {
return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world") && namedTunnel == nil) return c.IsSet("proxy-dns") && (!c.IsSet("hostname") && !c.IsSet("tag") && !c.IsSet("hello-world") && namedTunnel == nil)
} }
@ -152,44 +153,44 @@ func prepareTunnelConfig(
info *cliutil.BuildInfo, info *cliutil.BuildInfo,
log, logTransport *zerolog.Logger, log, logTransport *zerolog.Logger,
observer *connection.Observer, observer *connection.Observer,
namedTunnel *connection.NamedTunnelConfig, namedTunnel *connection.NamedTunnelProperties,
) (*origin.TunnelConfig, ingress.Ingress, error) { ) (*supervisor.TunnelConfig, *orchestration.Config, error) {
isNamedTunnel := namedTunnel != nil isNamedTunnel := namedTunnel != nil
configHostname := c.String("hostname") configHostname := c.String("hostname")
hostname, err := validation.ValidateHostname(configHostname) hostname, err := validation.ValidateHostname(configHostname)
if err != nil { if err != nil {
log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname") log.Err(err).Str(LogFieldHostname, configHostname).Msg("Invalid hostname")
return nil, ingress.Ingress{}, errors.Wrap(err, "Invalid hostname") return nil, nil, errors.Wrap(err, "Invalid hostname")
} }
clientID := c.String("id") clientID := c.String("id")
if !c.IsSet("id") { if !c.IsSet("id") {
clientID, err = generateRandomClientID(log) clientID, err = generateRandomClientID(log)
if err != nil { if err != nil {
return nil, ingress.Ingress{}, err return nil, nil, err
} }
} }
tags, err := NewTagSliceFromCLI(c.StringSlice("tag")) tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
if err != nil { if err != nil {
log.Err(err).Msg("Tag parse failure") log.Err(err).Msg("Tag parse failure")
return nil, ingress.Ingress{}, errors.Wrap(err, "Tag parse failure") return nil, nil, errors.Wrap(err, "Tag parse failure")
} }
tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID}) tags = append(tags, tunnelpogs.Tag{Name: "ID", Value: clientID})
var ( var (
ingressRules ingress.Ingress ingressRules ingress.Ingress
classicTunnel *connection.ClassicTunnelConfig classicTunnel *connection.ClassicTunnelProperties
) )
cfg := config.GetConfiguration() cfg := config.GetConfiguration()
if isNamedTunnel { if isNamedTunnel {
clientUUID, err := uuid.NewRandom() clientUUID, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, ingress.Ingress{}, errors.Wrap(err, "can't generate connector UUID") return nil, nil, errors.Wrap(err, "can't generate connector UUID")
} }
log.Info().Msgf("Generated Connector ID: %s", clientUUID) log.Info().Msgf("Generated Connector ID: %s", clientUUID)
features := append(c.StringSlice("features"), origin.FeatureSerializedHeaders) features := append(c.StringSlice("features"), supervisor.FeatureSerializedHeaders)
namedTunnel.Client = tunnelpogs.ClientInfo{ namedTunnel.Client = tunnelpogs.ClientInfo{
ClientID: clientUUID[:], ClientID: clientUUID[:],
Features: dedup(features), Features: dedup(features),
@ -198,10 +199,10 @@ func prepareTunnelConfig(
} }
ingressRules, err = ingress.ParseIngress(cfg) ingressRules, err = ingress.ParseIngress(cfg)
if err != nil && err != ingress.ErrNoIngressRules { if err != nil && err != ingress.ErrNoIngressRules {
return nil, ingress.Ingress{}, err return nil, nil, err
} }
if !ingressRules.IsEmpty() && c.IsSet("url") { if !ingressRules.IsEmpty() && c.IsSet("url") {
return nil, ingress.Ingress{}, ingress.ErrURLIncompatibleWithIngress return nil, nil, ingress.ErrURLIncompatibleWithIngress
} }
} else { } else {
@ -212,10 +213,10 @@ func prepareTunnelConfig(
originCert, err := getOriginCert(originCertPath, &originCertLog) originCert, err := getOriginCert(originCertPath, &originCertLog)
if err != nil { if err != nil {
return nil, ingress.Ingress{}, errors.Wrap(err, "Error getting origin cert") return nil, nil, errors.Wrap(err, "Error getting origin cert")
} }
classicTunnel = &connection.ClassicTunnelConfig{ classicTunnel = &connection.ClassicTunnelProperties{
Hostname: hostname, Hostname: hostname,
OriginCert: originCert, OriginCert: originCert,
// turn off use of reconnect token and auth refresh when using named tunnels // turn off use of reconnect token and auth refresh when using named tunnels
@ -227,20 +228,14 @@ func prepareTunnelConfig(
if ingressRules.IsEmpty() { if ingressRules.IsEmpty() {
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel) ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel)
if err != nil { if err != nil {
return nil, ingress.Ingress{}, err return nil, nil, err
} }
} }
var warpRoutingService *ingress.WarpRoutingService
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel) warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
if warpRoutingEnabled { protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.ProtocolPercentage, supervisor.ResolveTTL, log)
warpRoutingService = ingress.NewWarpRoutingService()
log.Info().Msgf("Warp-routing is enabled")
}
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.ProtocolPercentage, origin.ResolveTTL, log)
if err != nil { if err != nil {
return nil, ingress.Ingress{}, err return nil, nil, err
} }
log.Info().Msgf("Initial protocol %s", protocolSelector.Current()) log.Info().Msgf("Initial protocol %s", protocolSelector.Current())
@ -248,11 +243,11 @@ func prepareTunnelConfig(
for _, p := range connection.ProtocolList { for _, p := range connection.ProtocolList {
tlsSettings := p.TLSSettings() tlsSettings := p.TLSSettings()
if tlsSettings == nil { if tlsSettings == nil {
return nil, ingress.Ingress{}, fmt.Errorf("%s has unknown TLS settings", p) return nil, nil, fmt.Errorf("%s has unknown TLS settings", p)
} }
edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName) edgeTLSConfig, err := tlsconfig.CreateTunnelConfig(c, tlsSettings.ServerName)
if err != nil { if err != nil {
return nil, ingress.Ingress{}, errors.Wrap(err, "unable to create TLS config to connect with edge") return nil, nil, errors.Wrap(err, "unable to create TLS config to connect with edge")
} }
if len(tlsSettings.NextProtos) > 0 { if len(tlsSettings.NextProtos) > 0 {
edgeTLSConfig.NextProtos = tlsSettings.NextProtos edgeTLSConfig.NextProtos = tlsSettings.NextProtos
@ -260,15 +255,9 @@ func prepareTunnelConfig(
edgeTLSConfigs[p] = edgeTLSConfig edgeTLSConfigs[p] = edgeTLSConfig
} }
originProxy := origin.NewOriginProxy(ingressRules, warpRoutingService, tags, log)
gracePeriod, err := gracePeriod(c) gracePeriod, err := gracePeriod(c)
if err != nil { if err != nil {
return nil, ingress.Ingress{}, err return nil, nil, err
}
connectionConfig := &connection.Config{
OriginProxy: originProxy,
GracePeriod: gracePeriod,
ReplaceExisting: c.Bool("force"),
} }
muxerConfig := &connection.MuxerConfig{ muxerConfig := &connection.MuxerConfig{
HeartbeatInterval: c.Duration("heartbeat-interval"), HeartbeatInterval: c.Duration("heartbeat-interval"),
@ -279,21 +268,22 @@ func prepareTunnelConfig(
MetricsUpdateFreq: c.Duration("metrics-update-freq"), MetricsUpdateFreq: c.Duration("metrics-update-freq"),
} }
return &origin.TunnelConfig{ tunnelConfig := &supervisor.TunnelConfig{
ConnectionConfig: connectionConfig, GracePeriod: gracePeriod,
OSArch: info.OSArch(), ReplaceExisting: c.Bool("force"),
ClientID: clientID, OSArch: info.OSArch(),
EdgeAddrs: c.StringSlice("edge"), ClientID: clientID,
Region: c.String("region"), EdgeAddrs: c.StringSlice("edge"),
HAConnections: c.Int("ha-connections"), Region: c.String("region"),
IncidentLookup: origin.NewIncidentLookup(), HAConnections: c.Int("ha-connections"),
IsAutoupdated: c.Bool("is-autoupdated"), IncidentLookup: supervisor.NewIncidentLookup(),
LBPool: c.String("lb-pool"), IsAutoupdated: c.Bool("is-autoupdated"),
Tags: tags, LBPool: c.String("lb-pool"),
Log: log, Tags: tags,
LogTransport: logTransport, Log: log,
Observer: observer, LogTransport: logTransport,
ReportedVersion: info.Version(), Observer: observer,
ReportedVersion: info.Version(),
// Note TUN-3758 , we use Int because UInt is not supported with altsrc // Note TUN-3758 , we use Int because UInt is not supported with altsrc
Retries: uint(c.Int("retries")), Retries: uint(c.Int("retries")),
RunFromTerminal: isRunningFromTerminal(), RunFromTerminal: isRunningFromTerminal(),
@ -302,7 +292,12 @@ func prepareTunnelConfig(
MuxerConfig: muxerConfig, MuxerConfig: muxerConfig,
ProtocolSelector: protocolSelector, ProtocolSelector: protocolSelector,
EdgeTLSConfigs: edgeTLSConfigs, EdgeTLSConfigs: edgeTLSConfigs,
}, ingressRules, nil }
dynamicConfig := &orchestration.Config{
Ingress: &ingressRules,
WarpRoutingEnabled: warpRoutingEnabled,
}
return tunnelConfig, dynamicConfig, nil
} }
func gracePeriod(c *cli.Context) (time.Duration, error) { func gracePeriod(c *cli.Context) (time.Duration, error) {

View File

@ -1,6 +1,7 @@
package tunnel package tunnel
import ( import (
"encoding/json"
"fmt" "fmt"
"net/url" "net/url"
@ -12,6 +13,15 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const ingressDataJSONFlagName = "json"
var ingressDataJSON = &cli.StringFlag{
Name: ingressDataJSONFlagName,
Aliases: []string{"j"},
Usage: `Accepts data in the form of json as an input rather than read from a file`,
EnvVars: []string{"TUNNEL_INGRESS_VALIDATE_JSON"},
}
func buildIngressSubcommand() *cli.Command { func buildIngressSubcommand() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "ingress", Name: "ingress",
@ -49,6 +59,7 @@ func buildValidateIngressCommand() *cli.Command {
Usage: "Validate the ingress configuration ", Usage: "Validate the ingress configuration ",
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate", UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
Description: "Validates the configuration file, ensuring your ingress rules are OK.", Description: "Validates the configuration file, ensuring your ingress rules are OK.",
Flags: []cli.Flag{ingressDataJSON},
} }
} }
@ -69,12 +80,11 @@ func buildTestURLCommand() *cli.Command {
// validateIngressCommand check the syntax of the ingress rules in the cloudflared config file // validateIngressCommand check the syntax of the ingress rules in the cloudflared config file
func validateIngressCommand(c *cli.Context, warnings string) error { func validateIngressCommand(c *cli.Context, warnings string) error {
conf := config.GetConfiguration() conf, err := getConfiguration(c)
if conf.Source() == "" { if err != nil {
fmt.Println("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files") return err
return nil
} }
fmt.Println("Validating rules from", conf.Source())
if _, err := ingress.ParseIngress(conf); err != nil { if _, err := ingress.ParseIngress(conf); err != nil {
return errors.Wrap(err, "Validation failed") return errors.Wrap(err, "Validation failed")
} }
@ -90,6 +100,22 @@ func validateIngressCommand(c *cli.Context, warnings string) error {
return nil return nil
} }
func getConfiguration(c *cli.Context) (*config.Configuration, error) {
var conf *config.Configuration
if c.IsSet(ingressDataJSONFlagName) {
ingressJSON := c.String(ingressDataJSONFlagName)
fmt.Println("Validating rules from cmdline flag --json")
err := json.Unmarshal([]byte(ingressJSON), &conf)
return conf, err
}
conf = config.GetConfiguration()
if conf.Source() == "" {
return nil, errors.New("No configuration file was found. Please create one, or use the --config flag to specify its filepath. You can use the help command to learn more about configuration files")
}
fmt.Println("Validating rules from", conf.Source())
return conf, nil
}
// testURLCommand checks which ingress rule matches the given URL. // testURLCommand checks which ingress rule matches the given URL.
func testURLCommand(c *cli.Context) error { func testURLCommand(c *cli.Context) error {
requestArg := c.Args().First() requestArg := c.Args().First()

View File

@ -55,7 +55,6 @@ func RunQuickTunnel(sc *subcommandContext) error {
AccountTag: data.Result.AccountTag, AccountTag: data.Result.AccountTag,
TunnelSecret: data.Result.Secret, TunnelSecret: data.Result.Secret,
TunnelID: tunnelID, TunnelID: tunnelID,
TunnelName: data.Result.Name,
} }
url := data.Result.Hostname url := data.Result.Hostname
@ -77,7 +76,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
return StartServer( return StartServer(
sc.c, sc.c,
buildInfo, buildInfo,
&connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname}, &connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
sc.log, sc.log,
sc.isUIEnabled, sc.isUIEnabled,
) )

View File

@ -185,7 +185,6 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
AccountTag: credential.cert.AccountID, AccountTag: credential.cert.AccountID,
TunnelSecret: tunnelSecret, TunnelSecret: tunnelSecret,
TunnelID: tunnel.ID, TunnelID: tunnel.ID,
TunnelName: name,
} }
usedCertPath := false usedCertPath := false
if credentialsFilePath == "" { if credentialsFilePath == "" {
@ -221,7 +220,9 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
} }
fmt.Println(" Keep this file secret. To revoke these credentials, delete the tunnel.") fmt.Println(" Keep this file secret. To revoke these credentials, delete the tunnel.")
fmt.Printf("\nCreated tunnel %s with id %s\n", tunnel.Name, tunnel.ID) fmt.Printf("\nCreated tunnel %s with id %s\n", tunnel.Name, tunnel.ID)
return tunnel, nil fmt.Printf("\nTunnel Token: %s\n", tunnel.Token)
return &tunnel.Tunnel, nil
} }
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) { func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
@ -301,10 +302,16 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
return err return err
} }
return sc.runWithCredentials(credentials)
}
func (sc *subcommandContext) runWithCredentials(credentials connection.Credentials) error {
sc.log.Info().Str(LogFieldTunnelID, credentials.TunnelID.String()).Msg("Starting tunnel")
return StartServer( return StartServer(
sc.c, sc.c,
buildInfo, buildInfo,
&connection.NamedTunnelConfig{Credentials: credentials}, &connection.NamedTunnelProperties{Credentials: credentials},
sc.log, sc.log,
sc.isUIEnabled, sc.isUIEnabled,
) )
@ -370,7 +377,7 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
// Look up name in the credentials file. // Look up name in the credentials file.
credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs) credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs)
if credentials, err := sc.readTunnelCredentials(credFinder); err == nil { if credentials, err := sc.readTunnelCredentials(credFinder); err == nil {
if credentials.TunnelID != uuid.Nil && input == credentials.TunnelName { if credentials.TunnelID != uuid.Nil {
return credentials.TunnelID, nil return credentials.TunnelID, nil
} }
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cfapi"
@ -115,7 +116,6 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
AccountTag: accountTag, AccountTag: accountTag,
TunnelID: tunnelID, TunnelID: tunnelID,
TunnelSecret: secret, TunnelSecret: secret,
TunnelName: name,
}, },
}, },
{ {
@ -160,7 +160,6 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
AccountTag: accountTag, AccountTag: accountTag,
TunnelID: tunnelID, TunnelID: tunnelID,
TunnelSecret: secret, TunnelSecret: secret,
TunnelName: name,
}, },
}, },
} }
@ -322,3 +321,48 @@ func Test_subcommandContext_Delete(t *testing.T) {
}) })
} }
} }
func Test_subcommandContext_ValidateIngressCommand(t *testing.T) {
var tests = []struct {
name string
c *cli.Context
wantErr bool
expectedErr error
}{
{
name: "read a valid configuration from data",
c: func() *cli.Context {
data := `{ "warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}`
flagSet := flag.NewFlagSet("json", flag.PanicOnError)
flagSet.String(ingressDataJSONFlagName, data, "")
c := cli.NewContext(cli.NewApp(), flagSet, nil)
_ = c.Set(ingressDataJSONFlagName, data)
return c
}(),
},
{
name: "read an invalid configuration with multiple mistakes",
c: func() *cli.Context {
data := `{ "ingress" : [ {"hostname": "test", "service": "localhost:8000" } , {"service": "http_status:invalid_status"} ]}`
flagSet := flag.NewFlagSet("json", flag.PanicOnError)
flagSet.String(ingressDataJSONFlagName, data, "")
c := cli.NewContext(cli.NewApp(), flagSet, nil)
_ = c.Set(ingressDataJSONFlagName, data)
return c
}(),
wantErr: true,
expectedErr: errors.New("Validation failed: localhost:8000 is an invalid address, please make sure it has a scheme and a hostname"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateIngressCommand(tt.c, "")
if tt.wantErr {
assert.Equal(t, tt.expectedErr.Error(), err.Error())
} else {
assert.Nil(t, err)
}
})
}
}

View File

@ -2,6 +2,7 @@ package tunnel
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -34,6 +35,7 @@ const (
CredFileFlagAlias = "cred-file" CredFileFlagAlias = "cred-file"
CredFileFlag = "credentials-file" CredFileFlag = "credentials-file"
CredContentsFlag = "credentials-contents" CredContentsFlag = "credentials-contents"
TunnelTokenFlag = "token"
overwriteDNSFlagName = "overwrite-dns" overwriteDNSFlagName = "overwrite-dns"
LogFieldTunnelID = "tunnelID" LogFieldTunnelID = "tunnelID"
@ -118,6 +120,11 @@ var (
Usage: "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence.", Usage: "Contents of the tunnel credentials JSON file to use. When provided along with credentials-file, this will take precedence.",
EnvVars: []string{"TUNNEL_CRED_CONTENTS"}, EnvVars: []string{"TUNNEL_CRED_CONTENTS"},
}) })
tunnelTokenFlag = altsrc.NewStringFlag(&cli.StringFlag{
Name: TunnelTokenFlag,
Usage: "The Tunnel token. When provided along with credentials, this will take precedence.",
EnvVars: []string{"TUNNEL_TOKEN"},
})
forceDeleteFlag = &cli.BoolFlag{ forceDeleteFlag = &cli.BoolFlag{
Name: "force", Name: "force",
Aliases: []string{"f"}, Aliases: []string{"f"},
@ -597,6 +604,7 @@ func buildRunCommand() *cli.Command {
credentialsContentsFlag, credentialsContentsFlag,
selectProtocolFlag, selectProtocolFlag,
featuresFlag, featuresFlag,
tunnelTokenFlag,
} }
flags = append(flags, configureProxyFlags(false)...) flags = append(flags, configureProxyFlags(false)...)
return &cli.Command{ return &cli.Command{
@ -627,14 +635,6 @@ func runCommand(c *cli.Context) error {
if c.NArg() > 1 { if c.NArg() > 1 {
return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`) return cliutil.UsageError(`"cloudflared tunnel run" accepts only one argument, the ID or name of the tunnel to run.`)
} }
tunnelRef := c.Args().First()
if tunnelRef == "" {
// see if tunnel id was in the config file
tunnelRef = config.GetConfiguration().TunnelID
if tunnelRef == "" {
return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`)
}
}
if c.String("hostname") != "" { if c.String("hostname") != "" {
sc.log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " + sc.log.Warn().Msg("The property `hostname` in your configuration is ignored because you configured a Named Tunnel " +
@ -642,7 +642,38 @@ func runCommand(c *cli.Context) error {
"your origin will not be reachable. You should remove the `hostname` property to avoid this warning.") "your origin will not be reachable. You should remove the `hostname` property to avoid this warning.")
} }
return runNamedTunnel(sc, tunnelRef) // Check if token is provided and if not use default tunnelID flag method
if tokenStr := c.String(TunnelTokenFlag); tokenStr != "" {
if token, err := parseToken(tokenStr); err == nil {
return sc.runWithCredentials(token.Credentials())
}
return cliutil.UsageError("Provided Tunnel token is not valid.")
} else {
tunnelRef := c.Args().First()
if tunnelRef == "" {
// see if tunnel id was in the config file
tunnelRef = config.GetConfiguration().TunnelID
if tunnelRef == "" {
return cliutil.UsageError(`"cloudflared tunnel run" requires the ID or name of the tunnel to run as the last command line argument or in the configuration file.`)
}
}
return runNamedTunnel(sc, tunnelRef)
}
}
func parseToken(tokenStr string) (*connection.TunnelToken, error) {
content, err := base64.StdEncoding.DecodeString(tokenStr)
if err != nil {
return nil, err
}
var token connection.TunnelToken
if err := json.Unmarshal(content, &token); err != nil {
return nil, err
}
return &token, nil
} }
func runNamedTunnel(sc *subcommandContext, tunnelRef string) error { func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
@ -650,9 +681,6 @@ func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
if err != nil { if err != nil {
return errors.Wrap(err, "error parsing tunnel ID") return errors.Wrap(err, "error parsing tunnel ID")
} }
sc.log.Info().Str(LogFieldTunnelID, tunnelID.String()).Msg("Starting tunnel")
return sc.run(tunnelID) return sc.run(tunnelID)
} }

View File

@ -1,14 +1,18 @@
package tunnel package tunnel
import ( import (
"encoding/base64"
"encoding/json"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/google/uuid" "github.com/google/uuid"
homedir "github.com/mitchellh/go-homedir" homedir "github.com/mitchellh/go-homedir"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/cfapi" "github.com/cloudflare/cloudflared/cfapi"
"github.com/cloudflare/cloudflared/connection"
) )
func Test_fmtConnections(t *testing.T) { func Test_fmtConnections(t *testing.T) {
@ -177,3 +181,24 @@ func Test_validateHostname(t *testing.T) {
}) })
} }
} }
func Test_TunnelToken(t *testing.T) {
token, err := parseToken("aabc")
require.Error(t, err)
require.Nil(t, token)
expectedToken := &connection.TunnelToken{
AccountTag: "abc",
TunnelSecret: []byte("secret"),
TunnelID: uuid.New(),
}
tokenJsonStr, err := json.Marshal(expectedToken)
require.NoError(t, err)
token64 := base64.StdEncoding.EncodeToString(tokenJsonStr)
token, err = parseToken(token64)
require.NoError(t, err)
require.Equal(t, token, expectedToken)
}

View File

@ -19,7 +19,7 @@ import (
const ( const (
DefaultCheckUpdateFreq = time.Hour * 24 DefaultCheckUpdateFreq = time.Hour * 24
noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/run-as-service" noUpdateInShellMessage = "cloudflared will not automatically update when run from the shell. To enable auto-updates, run cloudflared as a service: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/run-tunnel/as-a-service/"
noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems." noUpdateOnWindowsMessage = "cloudflared will not automatically update on Windows systems."
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager." noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
isManagedInstallFile = ".installedFromPackageManager" isManagedInstallFile = ".installedFromPackageManager"

View File

@ -175,60 +175,62 @@ func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
} }
type UnvalidatedIngressRule struct { type UnvalidatedIngressRule struct {
Hostname string Hostname string `json:"hostname"`
Path string Path string `json:"path"`
Service string Service string `json:"service"`
OriginRequest OriginRequestConfig `yaml:"originRequest"` OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"`
} }
// OriginRequestConfig is a set of optional fields that users may set to // OriginRequestConfig is a set of optional fields that users may set to
// customize how cloudflared sends requests to origin services. It is used to set // customize how cloudflared sends requests to origin services. It is used to set
// up general config that apply to all rules, and also, specific per-rule // up general config that apply to all rules, and also, specific per-rule
// config. // config.
// Note: To specify a time.Duration in go-yaml, use e.g. "3s" or "24h". // Note:
// - To specify a time.Duration in go-yaml, use e.g. "3s" or "24h".
// - To specify a time.Duration in json, use int64 of the nanoseconds
type OriginRequestConfig struct { type OriginRequestConfig struct {
// HTTP proxy timeout for establishing a new connection // HTTP proxy timeout for establishing a new connection
ConnectTimeout *time.Duration `yaml:"connectTimeout"` ConnectTimeout *time.Duration `yaml:"connectTimeout" json:"connectTimeout"`
// HTTP proxy timeout for completing a TLS handshake // HTTP proxy timeout for completing a TLS handshake
TLSTimeout *time.Duration `yaml:"tlsTimeout"` TLSTimeout *time.Duration `yaml:"tlsTimeout" json:"tlsTimeout"`
// HTTP proxy TCP keepalive duration // HTTP proxy TCP keepalive duration
TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"` TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs *bool `yaml:"noHappyEyeballs"` NoHappyEyeballs *bool `yaml:"noHappyEyeballs" json:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size // HTTP proxy maximum keepalive connection pool size
KeepAliveConnections *int `yaml:"keepAliveConnections"` KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection // HTTP proxy timeout for closing an idle connection
KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"` KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"`
// Sets the HTTP Host header for the local webserver. // Sets the HTTP Host header for the local webserver.
HTTPHostHeader *string `yaml:"httpHostHeader"` HTTPHostHeader *string `yaml:"httpHostHeader" json:"httpHostHeader"`
// Hostname on the origin server certificate. // Hostname on the origin server certificate.
OriginServerName *string `yaml:"originServerName"` OriginServerName *string `yaml:"originServerName" json:"originServerName"`
// Path to the CA for the certificate of your origin. // Path to the CA for the certificate of your origin.
// This option should be used only if your certificate is not signed by Cloudflare. // This option should be used only if your certificate is not signed by Cloudflare.
CAPool *string `yaml:"caPool"` CAPool *string `yaml:"caPool" json:"caPool"`
// Disables TLS verification of the certificate presented by your origin. // Disables TLS verification of the certificate presented by your origin.
// Will allow any certificate from the origin to be accepted. // Will allow any certificate from the origin to be accepted.
// Note: The connection from your machine to Cloudflare's Edge is still encrypted. // Note: The connection from your machine to Cloudflare's Edge is still encrypted.
NoTLSVerify *bool `yaml:"noTLSVerify"` NoTLSVerify *bool `yaml:"noTLSVerify" json:"noTLSVerify"`
// Disables chunked transfer encoding. // Disables chunked transfer encoding.
// Useful if you are running a WSGI server. // Useful if you are running a WSGI server.
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"` DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding"`
// Runs as jump host // Runs as jump host
BastionMode *bool `yaml:"bastionMode"` BastionMode *bool `yaml:"bastionMode" json:"bastionMode"`
// Listen address for the proxy. // Listen address for the proxy.
ProxyAddress *string `yaml:"proxyAddress"` ProxyAddress *string `yaml:"proxyAddress" json:"proxyAddress"`
// Listen port for the proxy. // Listen port for the proxy.
ProxyPort *uint `yaml:"proxyPort"` ProxyPort *uint `yaml:"proxyPort" json:"proxyPort"`
// Valid options are 'socks' or empty. // Valid options are 'socks' or empty.
ProxyType *string `yaml:"proxyType"` ProxyType *string `yaml:"proxyType" json:"proxyType"`
// IP rules for the proxy service // IP rules for the proxy service
IPRules []IngressIPRule `yaml:"ipRules"` IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules"`
} }
type IngressIPRule struct { type IngressIPRule struct {
Prefix *string `yaml:"prefix"` Prefix *string `yaml:"prefix" json:"prefix"`
Ports []int `yaml:"ports"` Ports []int `yaml:"ports" json:"ports"`
Allow bool `yaml:"allow"` Allow bool `yaml:"allow" json:"allow"`
} }
type Configuration struct { type Configuration struct {
@ -240,7 +242,7 @@ type Configuration struct {
} }
type WarpRoutingConfig struct { type WarpRoutingConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled" json:"enabled"`
} }
type configFileSettings struct { type configFileSettings struct {

View File

@ -1,6 +1,7 @@
package config package config
import ( import (
"encoding/json"
"testing" "testing"
"time" "time"
@ -26,6 +27,18 @@ func TestConfigFileSettings(t *testing.T) {
) )
rawYAML := ` rawYAML := `
tunnel: config-file-test tunnel: config-file-test
originRequest:
ipRules:
- prefix: "10.0.0.0/8"
ports:
- 80
- 8080
allow: false
- prefix: "fc00::/7"
ports:
- 443
- 4443
allow: true
ingress: ingress:
- hostname: tunnel1.example.com - hostname: tunnel1.example.com
path: /id path: /id
@ -53,6 +66,21 @@ counters:
assert.Equal(t, firstIngress, config.Ingress[0]) assert.Equal(t, firstIngress, config.Ingress[0])
assert.Equal(t, secondIngress, config.Ingress[1]) assert.Equal(t, secondIngress, config.Ingress[1])
assert.Equal(t, warpRouting, config.WarpRouting) assert.Equal(t, warpRouting, config.WarpRouting)
privateV4 := "10.0.0.0/8"
privateV6 := "fc00::/7"
ipRules := []IngressIPRule{
{
Prefix: &privateV4,
Ports: []int{80, 8080},
Allow: false,
},
{
Prefix: &privateV6,
Ports: []int{443, 4443},
Allow: true,
},
}
assert.Equal(t, ipRules, config.OriginRequest.IPRules)
retries, err := config.Int("retries") retries, err := config.Int("retries")
assert.NoError(t, err) assert.NoError(t, err)
@ -81,3 +109,71 @@ counters:
assert.Equal(t, 456, counters[1]) assert.Equal(t, 456, counters[1])
} }
func TestUnmarshalOriginRequestConfig(t *testing.T) {
raw := []byte(`
{
"connectTimeout": 10000000000,
"tlsTimeout": 30000000000,
"tcpKeepAlive": 30000000000,
"noHappyEyeballs": true,
"keepAliveTimeout": 60000000000,
"keepAliveConnections": 10,
"httpHostHeader": "app.tunnel.com",
"originServerName": "app.tunnel.com",
"caPool": "/etc/capool",
"noTLSVerify": true,
"disableChunkedEncoding": true,
"bastionMode": true,
"proxyAddress": "127.0.0.3",
"proxyPort": 9000,
"proxyType": "socks",
"ipRules": [
{
"prefix": "10.0.0.0/8",
"ports": [80, 8080],
"allow": false
},
{
"prefix": "fc00::/7",
"ports": [443, 4443],
"allow": true
}
]
}
`)
var config OriginRequestConfig
assert.NoError(t, json.Unmarshal(raw, &config))
assert.Equal(t, time.Second*10, *config.ConnectTimeout)
assert.Equal(t, time.Second*30, *config.TLSTimeout)
assert.Equal(t, time.Second*30, *config.TCPKeepAlive)
assert.Equal(t, true, *config.NoHappyEyeballs)
assert.Equal(t, time.Second*60, *config.KeepAliveTimeout)
assert.Equal(t, 10, *config.KeepAliveConnections)
assert.Equal(t, "app.tunnel.com", *config.HTTPHostHeader)
assert.Equal(t, "app.tunnel.com", *config.OriginServerName)
assert.Equal(t, "/etc/capool", *config.CAPool)
assert.Equal(t, true, *config.NoTLSVerify)
assert.Equal(t, true, *config.DisableChunkedEncoding)
assert.Equal(t, true, *config.BastionMode)
assert.Equal(t, "127.0.0.3", *config.ProxyAddress)
assert.Equal(t, true, *config.NoTLSVerify)
assert.Equal(t, uint(9000), *config.ProxyPort)
assert.Equal(t, "socks", *config.ProxyType)
privateV4 := "10.0.0.0/8"
privateV6 := "fc00::/7"
ipRules := []IngressIPRule{
{
Prefix: &privateV4,
Ports: []int{80, 8080},
Allow: false,
},
{
Prefix: &privateV6,
Ports: []int{443, 4443},
Allow: true,
},
}
assert.Equal(t, ipRules, config.IPRules)
}

View File

@ -25,13 +25,12 @@ const (
var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols)) var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
type Config struct { type Orchestrator interface {
OriginProxy OriginProxy UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
GracePeriod time.Duration GetOriginProxy() (OriginProxy, error)
ReplaceExisting bool
} }
type NamedTunnelConfig struct { type NamedTunnelProperties struct {
Credentials Credentials Credentials Credentials
Client pogs.ClientInfo Client pogs.ClientInfo
QuickTunnelUrl string QuickTunnelUrl string
@ -42,7 +41,6 @@ type Credentials struct {
AccountTag string AccountTag string
TunnelSecret []byte TunnelSecret []byte
TunnelID uuid.UUID TunnelID uuid.UUID
TunnelName string
} }
func (c *Credentials) Auth() pogs.TunnelAuth { func (c *Credentials) Auth() pogs.TunnelAuth {
@ -52,7 +50,22 @@ func (c *Credentials) Auth() pogs.TunnelAuth {
} }
} }
type ClassicTunnelConfig struct { // TunnelToken are Credentials but encoded with custom fields namings.
type TunnelToken struct {
AccountTag string `json:"a"`
TunnelSecret []byte `json:"s"`
TunnelID uuid.UUID `json:"t"`
}
func (t TunnelToken) Credentials() Credentials {
return Credentials{
AccountTag: t.AccountTag,
TunnelSecret: t.TunnelSecret,
TunnelID: t.TunnelID,
}
}
type ClassicTunnelProperties struct {
Hostname string Hostname string
OriginCert []byte OriginCert []byte
// feature-flag to use new edge reconnect tokens // feature-flag to use new edge reconnect tokens

View File

@ -4,33 +4,28 @@ import (
"context" "context"
"fmt" "fmt"
"io" "io"
"math/rand"
"net/http" "net/http"
"net/url"
"testing" "testing"
"time" "time"
"github.com/gobwas/ws/wsutil"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/cloudflare/cloudflared/ingress" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/websocket"
) )
const ( const (
largeFileSize = 2 * 1024 * 1024 largeFileSize = 2 * 1024 * 1024
testGracePeriod = time.Millisecond * 100
) )
var ( var (
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil) testOrchestrator = &mockOrchestrator{
testConfig = &Config{ originProxy: &mockOriginProxy{},
OriginProxy: &mockOriginProxy{},
GracePeriod: time.Millisecond * 100,
} }
log = zerolog.Nop() log = zerolog.Nop()
testOriginURL = &url.URL{
Scheme: "https",
Host: "connectiontest.argotunnel.com",
}
testLargeResp = make([]byte, largeFileSize) testLargeResp = make([]byte, largeFileSize)
) )
@ -42,6 +37,20 @@ type testRequest struct {
isProxyError bool isProxyError bool
} }
type mockOrchestrator struct {
originProxy OriginProxy
}
func (*mockOrchestrator) UpdateConfig(version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
return &tunnelpogs.UpdateConfigurationResponse{
LastAppliedVersion: version,
}
}
func (mcr *mockOrchestrator) GetOriginProxy() (OriginProxy, error) {
return mcr.originProxy, nil
}
type mockOriginProxy struct{} type mockOriginProxy struct{}
func (moc *mockOriginProxy) ProxyHTTP( func (moc *mockOriginProxy) ProxyHTTP(
@ -50,7 +59,15 @@ func (moc *mockOriginProxy) ProxyHTTP(
isWebsocket bool, isWebsocket bool,
) error { ) error {
if isWebsocket { if isWebsocket {
return wsEndpoint(w, req) switch req.URL.Path {
case "/ws/echo":
return wsEchoEndpoint(w, req)
case "/ws/flaky":
return wsFlakyEndpoint(w, req)
default:
originRespEndpoint(w, http.StatusNotFound, []byte("ws endpoint not found"))
return fmt.Errorf("Unknwon websocket endpoint %s", req.URL.Path)
}
} }
switch req.URL.Path { switch req.URL.Path {
case "/ok": case "/ok":
@ -78,32 +95,82 @@ func (moc *mockOriginProxy) ProxyTCP(
return nil return nil
} }
type nowriter struct { type echoPipe struct {
io.Reader reader *io.PipeReader
writer *io.PipeWriter
} }
func (nowriter) Write(p []byte) (int, error) { func (ep *echoPipe) Read(p []byte) (int, error) {
return 0, fmt.Errorf("Writer not implemented") return ep.reader.Read(p)
} }
func wsEndpoint(w ResponseWriter, r *http.Request) error { func (ep *echoPipe) Write(p []byte) (int, error) {
return ep.writer.Write(p)
}
// A mock origin that echos data by streaming like a tcpOverWSConnection
// https://github.com/cloudflare/cloudflared/blob/master/ingress/origin_connection.go
func wsEchoEndpoint(w ResponseWriter, r *http.Request) error {
resp := &http.Response{ resp := &http.Response{
StatusCode: http.StatusSwitchingProtocols, StatusCode: http.StatusSwitchingProtocols,
} }
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header) if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
clientReader := nowriter{r.Body} return err
}
wsCtx, cancel := context.WithCancel(r.Context())
readPipe, writePipe := io.Pipe()
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
go func() { go func() {
for { select {
data, err := wsutil.ReadClientText(clientReader) case <-wsCtx.Done():
if err != nil { case <-r.Context().Done():
return
}
if err := wsutil.WriteServerText(w, data); err != nil {
return
}
} }
readPipe.Close()
writePipe.Close()
}() }()
<-r.Context().Done()
originConn := &echoPipe{reader: readPipe, writer: writePipe}
websocket.Stream(wsConn, originConn, &log)
cancel()
wsConn.Close()
return nil
}
type flakyConn struct {
closeAt time.Time
}
func (fc *flakyConn) Read(p []byte) (int, error) {
if time.Now().After(fc.closeAt) {
return 0, io.EOF
}
n := copy(p, "Read from flaky connection")
return n, nil
}
func (fc *flakyConn) Write(p []byte) (int, error) {
if time.Now().After(fc.closeAt) {
return 0, fmt.Errorf("flaky connection closed")
}
return len(p), nil
}
func wsFlakyEndpoint(w ResponseWriter, r *http.Request) error {
resp := &http.Response{
StatusCode: http.StatusSwitchingProtocols,
}
if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
return err
}
wsCtx, cancel := context.WithCancel(r.Context())
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
closedAfter := time.Millisecond * time.Duration(rand.Intn(50))
originConn := &flakyConn{closeAt: time.Now().Add(closedAfter)}
websocket.Stream(wsConn, originConn, &log)
cancel()
wsConn.Close()
return nil return nil
} }

View File

@ -16,9 +16,9 @@ type RPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) Na
type controlStream struct { type controlStream struct {
observer *Observer observer *Observer
connectedFuse ConnectedFuse connectedFuse ConnectedFuse
namedTunnelConfig *NamedTunnelConfig namedTunnelProperties *NamedTunnelProperties
connIndex uint8 connIndex uint8
newRPCClientFunc RPCClientFunc newRPCClientFunc RPCClientFunc
@ -39,7 +39,7 @@ type ControlStreamHandler interface {
func NewControlStream( func NewControlStream(
observer *Observer, observer *Observer,
connectedFuse ConnectedFuse, connectedFuse ConnectedFuse,
namedTunnelConfig *NamedTunnelConfig, namedTunnelConfig *NamedTunnelProperties,
connIndex uint8, connIndex uint8,
newRPCClientFunc RPCClientFunc, newRPCClientFunc RPCClientFunc,
gracefulShutdownC <-chan struct{}, gracefulShutdownC <-chan struct{},
@ -49,13 +49,13 @@ func NewControlStream(
newRPCClientFunc = newRegistrationRPCClient newRPCClientFunc = newRegistrationRPCClient
} }
return &controlStream{ return &controlStream{
observer: observer, observer: observer,
connectedFuse: connectedFuse, connectedFuse: connectedFuse,
namedTunnelConfig: namedTunnelConfig, namedTunnelProperties: namedTunnelConfig,
newRPCClientFunc: newRPCClientFunc, newRPCClientFunc: newRPCClientFunc,
connIndex: connIndex, connIndex: connIndex,
gracefulShutdownC: gracefulShutdownC, gracefulShutdownC: gracefulShutdownC,
gracePeriod: gracePeriod, gracePeriod: gracePeriod,
} }
} }
@ -66,7 +66,7 @@ func (c *controlStream) ServeControlStream(
) error { ) error {
rpcClient := c.newRPCClientFunc(ctx, rw, c.observer.log) rpcClient := c.newRPCClientFunc(ctx, rw, c.observer.log)
if err := rpcClient.RegisterConnection(ctx, c.namedTunnelConfig, connOptions, c.connIndex, c.observer); err != nil { if err := rpcClient.RegisterConnection(ctx, c.namedTunnelProperties, connOptions, c.connIndex, c.observer); err != nil {
rpcClient.Close() rpcClient.Close()
return err return err
} }

View File

@ -22,9 +22,10 @@ const (
) )
type h2muxConnection struct { type h2muxConnection struct {
config *Config orchestrator Orchestrator
muxerConfig *MuxerConfig gracePeriod time.Duration
muxer *h2mux.Muxer muxerConfig *MuxerConfig
muxer *h2mux.Muxer
// connectionID is only used by metrics, and prometheus requires labels to be string // connectionID is only used by metrics, and prometheus requires labels to be string
connIndexStr string connIndexStr string
connIndex uint8 connIndex uint8
@ -60,7 +61,8 @@ func (mc *MuxerConfig) H2MuxerConfig(h h2mux.MuxedStreamHandler, log *zerolog.Lo
// NewTunnelHandler returns a TunnelHandler, origin LAN IP and error // NewTunnelHandler returns a TunnelHandler, origin LAN IP and error
func NewH2muxConnection( func NewH2muxConnection(
config *Config, orchestrator Orchestrator,
gracePeriod time.Duration,
muxerConfig *MuxerConfig, muxerConfig *MuxerConfig,
edgeConn net.Conn, edgeConn net.Conn,
connIndex uint8, connIndex uint8,
@ -68,7 +70,8 @@ func NewH2muxConnection(
gracefulShutdownC <-chan struct{}, gracefulShutdownC <-chan struct{},
) (*h2muxConnection, error, bool) { ) (*h2muxConnection, error, bool) {
h := &h2muxConnection{ h := &h2muxConnection{
config: config, orchestrator: orchestrator,
gracePeriod: gracePeriod,
muxerConfig: muxerConfig, muxerConfig: muxerConfig,
connIndexStr: uint8ToString(connIndex), connIndexStr: uint8ToString(connIndex),
connIndex: connIndex, connIndex: connIndex,
@ -88,7 +91,7 @@ func NewH2muxConnection(
return h, nil, false return h, nil, false
} }
func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *NamedTunnelConfig, connOptions *tunnelpogs.ConnectionOptions, connectedFuse ConnectedFuse) error { func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *NamedTunnelProperties, connOptions *tunnelpogs.ConnectionOptions, connectedFuse ConnectedFuse) error {
errGroup, serveCtx := errgroup.WithContext(ctx) errGroup, serveCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error { errGroup.Go(func() error {
return h.serveMuxer(serveCtx) return h.serveMuxer(serveCtx)
@ -117,7 +120,7 @@ func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *Nam
return err return err
} }
func (h *h2muxConnection) ServeClassicTunnel(ctx context.Context, classicTunnel *ClassicTunnelConfig, credentialManager CredentialManager, registrationOptions *tunnelpogs.RegistrationOptions, connectedFuse ConnectedFuse) error { func (h *h2muxConnection) ServeClassicTunnel(ctx context.Context, classicTunnel *ClassicTunnelProperties, credentialManager CredentialManager, registrationOptions *tunnelpogs.RegistrationOptions, connectedFuse ConnectedFuse) error {
errGroup, serveCtx := errgroup.WithContext(ctx) errGroup, serveCtx := errgroup.WithContext(ctx)
errGroup.Go(func() error { errGroup.Go(func() error {
return h.serveMuxer(serveCtx) return h.serveMuxer(serveCtx)
@ -224,7 +227,13 @@ func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
sourceConnectionType = TypeWebsocket sourceConnectionType = TypeWebsocket
} }
err := h.config.OriginProxy.ProxyHTTP(respWriter, req, sourceConnectionType == TypeWebsocket) originProxy, err := h.orchestrator.GetOriginProxy()
if err != nil {
respWriter.WriteErrorResponse()
return err
}
err = originProxy.ProxyHTTP(respWriter, req, sourceConnectionType == TypeWebsocket)
if err != nil { if err != nil {
respWriter.WriteErrorResponse() respWriter.WriteErrorResponse()
} }

View File

@ -48,7 +48,7 @@ func newH2MuxConnection(t require.TestingT) (*h2muxConnection, *h2mux.Muxer) {
}() }()
var connIndex = uint8(0) var connIndex = uint8(0)
testObserver := NewObserver(&log, &log, false) testObserver := NewObserver(&log, &log, false)
h2muxConn, err, _ := NewH2muxConnection(testConfig, testMuxerConfig, originConn, connIndex, testObserver, nil) h2muxConn, err, _ := NewH2muxConnection(testOrchestrator, testGracePeriod, testMuxerConfig, originConn, connIndex, testObserver, nil)
require.NoError(t, err) require.NoError(t, err)
return h2muxConn, <-edgeMuxChan return h2muxConn, <-edgeMuxChan
} }
@ -147,7 +147,7 @@ func TestServeStreamWS(t *testing.T) {
headers := []h2mux.Header{ headers := []h2mux.Header{
{ {
Name: ":path", Name: ":path",
Value: "/ws", Value: "/ws/echo",
}, },
{ {
Name: "connection", Name: "connection",
@ -167,10 +167,10 @@ func TestServeStreamWS(t *testing.T) {
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin)) assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
data := []byte("test websocket") data := []byte("test websocket")
err = wsutil.WriteClientText(writePipe, data) err = wsutil.WriteClientBinary(writePipe, data)
require.NoError(t, err) require.NoError(t, err)
respBody, err := wsutil.ReadServerText(stream) respBody, err := wsutil.ReadServerBinary(stream)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody))) require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))

View File

@ -30,12 +30,12 @@ var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
// HTTP2Connection represents a net.Conn that uses HTTP2 frames to proxy traffic from the edge to cloudflared on the // HTTP2Connection represents a net.Conn that uses HTTP2 frames to proxy traffic from the edge to cloudflared on the
// origin. // origin.
type HTTP2Connection struct { type HTTP2Connection struct {
conn net.Conn conn net.Conn
server *http2.Server server *http2.Server
config *Config orchestrator Orchestrator
connOptions *tunnelpogs.ConnectionOptions connOptions *tunnelpogs.ConnectionOptions
observer *Observer observer *Observer
connIndex uint8 connIndex uint8
// newRPCClientFunc allows us to mock RPCs during testing // newRPCClientFunc allows us to mock RPCs during testing
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
@ -49,7 +49,7 @@ type HTTP2Connection struct {
// NewHTTP2Connection returns a new instance of HTTP2Connection. // NewHTTP2Connection returns a new instance of HTTP2Connection.
func NewHTTP2Connection( func NewHTTP2Connection(
conn net.Conn, conn net.Conn,
config *Config, orchestrator Orchestrator,
connOptions *tunnelpogs.ConnectionOptions, connOptions *tunnelpogs.ConnectionOptions,
observer *Observer, observer *Observer,
connIndex uint8, connIndex uint8,
@ -61,7 +61,7 @@ func NewHTTP2Connection(
server: &http2.Server{ server: &http2.Server{
MaxConcurrentStreams: MaxConcurrentStreams, MaxConcurrentStreams: MaxConcurrentStreams,
}, },
config: config, orchestrator: orchestrator,
connOptions: connOptions, connOptions: connOptions,
observer: observer, observer: observer,
connIndex: connIndex, connIndex: connIndex,
@ -106,6 +106,12 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
originProxy, err := c.orchestrator.GetOriginProxy()
if err != nil {
c.observer.log.Error().Msg(err.Error())
return
}
switch connType { switch connType {
case TypeControlStream: case TypeControlStream:
if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions); err != nil { if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions); err != nil {
@ -116,7 +122,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case TypeWebsocket, TypeHTTP: case TypeWebsocket, TypeHTTP:
stripWebsocketUpgradeHeader(r) stripWebsocketUpgradeHeader(r)
if err := c.config.OriginProxy.ProxyHTTP(respWriter, r, connType == TypeWebsocket); err != nil { if err := originProxy.ProxyHTTP(respWriter, r, connType == TypeWebsocket); err != nil {
err := fmt.Errorf("Failed to proxy HTTP: %w", err) err := fmt.Errorf("Failed to proxy HTTP: %w", err)
c.log.Error().Err(err) c.log.Error().Err(err)
respWriter.WriteErrorResponse() respWriter.WriteErrorResponse()
@ -131,7 +137,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
rws := NewHTTPResponseReadWriterAcker(respWriter, r) rws := NewHTTPResponseReadWriterAcker(respWriter, r)
if err := c.config.OriginProxy.ProxyTCP(r.Context(), rws, &TCPRequest{ if err := originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
Dest: host, Dest: host,
CFRay: FindCfRayHeader(r), CFRay: FindCfRayHeader(r),
LBProbe: IsLBProbeRequest(r), LBProbe: IsLBProbeRequest(r),

View File

@ -2,6 +2,7 @@ package connection
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -27,22 +28,23 @@ var (
) )
func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) { func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
edgeConn, originConn := net.Pipe() edgeConn, cfdConn := net.Pipe()
var connIndex = uint8(0) var connIndex = uint8(0)
log := zerolog.Nop() log := zerolog.Nop()
obs := NewObserver(&log, &log, false) obs := NewObserver(&log, &log, false)
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelConfig{}, &NamedTunnelProperties{},
connIndex, connIndex,
nil, nil,
nil, nil,
1*time.Second, 1*time.Second,
) )
return NewHTTP2Connection( return NewHTTP2Connection(
originConn, cfdConn,
testConfig, // OriginProxy is set in testConfigManager
testOrchestrator,
&pogs.ConnectionOptions{}, &pogs.ConnectionOptions{},
obs, obs,
connIndex, connIndex,
@ -130,7 +132,7 @@ type mockNamedTunnelRPCClient struct {
func (mc mockNamedTunnelRPCClient) RegisterConnection( func (mc mockNamedTunnelRPCClient) RegisterConnection(
c context.Context, c context.Context,
config *NamedTunnelConfig, properties *NamedTunnelProperties,
options *tunnelpogs.ConnectionOptions, options *tunnelpogs.ConnectionOptions,
connIndex uint8, connIndex uint8,
observer *Observer, observer *Observer,
@ -166,6 +168,8 @@ type wsRespWriter struct {
*httptest.ResponseRecorder *httptest.ResponseRecorder
readPipe *io.PipeReader readPipe *io.PipeReader
writePipe *io.PipeWriter writePipe *io.PipeWriter
closed bool
panicked bool
} }
func newWSRespWriter() *wsRespWriter { func newWSRespWriter() *wsRespWriter {
@ -174,46 +178,59 @@ func newWSRespWriter() *wsRespWriter {
httptest.NewRecorder(), httptest.NewRecorder(),
readPipe, readPipe,
writePipe, writePipe,
false,
false,
} }
} }
type nowriter struct {
io.Reader
}
func (nowriter) Write(_ []byte) (int, error) {
return 0, fmt.Errorf("writer not implemented")
}
func (w *wsRespWriter) RespBody() io.ReadWriter { func (w *wsRespWriter) RespBody() io.ReadWriter {
return nowriter{w.readPipe} return nowriter{w.readPipe}
} }
func (w *wsRespWriter) Write(data []byte) (n int, err error) { func (w *wsRespWriter) Write(data []byte) (n int, err error) {
if w.closed {
w.panicked = true
return 0, errors.New("wsRespWriter panicked")
}
return w.writePipe.Write(data) return w.writePipe.Write(data)
} }
func (w *wsRespWriter) close() {
w.closed = true
}
func TestServeWS(t *testing.T) { func TestServeWS(t *testing.T) {
http2Conn, _ := newTestHTTP2Connection() http2Conn, _ := newTestHTTP2Connection()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
http2Conn.Serve(ctx)
}()
respWriter := newWSRespWriter() respWriter := newWSRespWriter()
readPipe, writePipe := io.Pipe() readPipe, writePipe := io.Pipe()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws", readPipe) req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost:8080/ws/echo", readPipe)
require.NoError(t, err) require.NoError(t, err)
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade) req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
wg.Add(1) serveDone := make(chan struct{})
go func() { go func() {
defer wg.Done() defer close(serveDone)
http2Conn.ServeHTTP(respWriter, req) http2Conn.ServeHTTP(respWriter, req)
respWriter.close()
}() }()
data := []byte("test websocket") data := []byte("test websocket")
err = wsutil.WriteClientText(writePipe, data) err = wsutil.WriteClientBinary(writePipe, data)
require.NoError(t, err) require.NoError(t, err)
respBody, err := wsutil.ReadServerText(respWriter.RespBody()) respBody, err := wsutil.ReadServerBinary(respWriter.RespBody())
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody))) require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
@ -223,7 +240,65 @@ func TestServeWS(t *testing.T) {
require.Equal(t, http.StatusOK, resp.StatusCode) require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader)) require.Equal(t, responseMetaHeaderOrigin, resp.Header.Get(ResponseMetaHeader))
<-serveDone
require.False(t, respWriter.panicked)
}
// TestNoWriteAfterServeHTTPReturns is a regression test of https://jira.cfops.it/browse/TUN-5184
// to make sure we don't write to the ResponseWriter after the ServeHTTP method returns
func TestNoWriteAfterServeHTTPReturns(t *testing.T) {
cfdHTTP2Conn, edgeTCPConn := newTestHTTP2Connection()
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
serverDone := make(chan struct{})
go func() {
defer close(serverDone)
cfdHTTP2Conn.Serve(ctx)
}()
edgeTransport := http2.Transport{}
edgeHTTP2Conn, err := edgeTransport.NewClientConn(edgeTCPConn)
require.NoError(t, err)
message := []byte(t.Name())
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
readPipe, writePipe := io.Pipe()
reqCtx, reqCancel := context.WithCancel(ctx)
req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, "http://localhost:8080/ws/flaky", readPipe)
require.NoError(t, err)
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
resp, err := edgeHTTP2Conn.RoundTrip(req)
require.NoError(t, err)
// http2RespWriter should rewrite status 101 to 200
require.Equal(t, http.StatusOK, resp.StatusCode)
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-reqCtx.Done():
return
default:
}
_ = wsutil.WriteClientBinary(writePipe, message)
}
}()
time.Sleep(time.Millisecond * 100)
reqCancel()
}()
}
wg.Wait() wg.Wait()
cancel()
<-serverDone
} }
func TestServeControlStream(t *testing.T) { func TestServeControlStream(t *testing.T) {
@ -238,7 +313,7 @@ func TestServeControlStream(t *testing.T) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelConfig{}, &NamedTunnelProperties{},
1, 1,
rpcClientFactory.newMockRPCClient, rpcClientFactory.newMockRPCClient,
nil, nil,
@ -288,7 +363,7 @@ func TestFailRegistration(t *testing.T) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelConfig{}, &NamedTunnelProperties{},
http2Conn.connIndex, http2Conn.connIndex,
rpcClientFactory.newMockRPCClient, rpcClientFactory.newMockRPCClient,
nil, nil,
@ -334,7 +409,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
controlStream := NewControlStream( controlStream := NewControlStream(
obs, obs,
mockConnectedFuse{}, mockConnectedFuse{},
&NamedTunnelConfig{}, &NamedTunnelProperties{},
http2Conn.connIndex, http2Conn.connIndex,
rpcClientFactory.newMockRPCClient, rpcClientFactory.newMockRPCClient,
shutdownC, shutdownC,

View File

@ -195,7 +195,7 @@ type PercentageFetcher func() (edgediscovery.ProtocolPercents, error)
func NewProtocolSelector( func NewProtocolSelector(
protocolFlag string, protocolFlag string,
warpRoutingEnabled bool, warpRoutingEnabled bool,
namedTunnel *NamedTunnelConfig, namedTunnel *NamedTunnelProperties,
fetchFunc PercentageFetcher, fetchFunc PercentageFetcher,
ttl time.Duration, ttl time.Duration,
log *zerolog.Logger, log *zerolog.Logger,

View File

@ -16,7 +16,7 @@ const (
) )
var ( var (
testNamedTunnelConfig = &NamedTunnelConfig{ testNamedTunnelProperties = &NamedTunnelProperties{
Credentials: Credentials{ Credentials: Credentials{
AccountTag: "testAccountTag", AccountTag: "testAccountTag",
}, },
@ -51,7 +51,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback bool hasFallback bool
expectedFallback Protocol expectedFallback Protocol
warpRoutingEnabled bool warpRoutingEnabled bool
namedTunnelConfig *NamedTunnelConfig namedTunnelConfig *NamedTunnelProperties
fetchFunc PercentageFetcher fetchFunc PercentageFetcher
wantErr bool wantErr bool
}{ }{
@ -66,35 +66,35 @@ func TestNewProtocolSelector(t *testing.T) {
protocol: "h2mux", protocol: "h2mux",
expectedProtocol: H2mux, expectedProtocol: H2mux,
fetchFunc: func() (edgediscovery.ProtocolPercents, error) { return nil, nil }, fetchFunc: func() (edgediscovery.ProtocolPercents, error) { return nil, nil },
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel over http2", name: "named tunnel over http2",
protocol: "http2", protocol: "http2",
expectedProtocol: HTTP2, expectedProtocol: HTTP2,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel http2 disabled still gets http2 because it is manually picked", name: "named tunnel http2 disabled still gets http2 because it is manually picked",
protocol: "http2", protocol: "http2",
expectedProtocol: HTTP2, expectedProtocol: HTTP2,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel quic disabled still gets quic because it is manually picked", name: "named tunnel quic disabled still gets quic because it is manually picked",
protocol: "quic", protocol: "quic",
expectedProtocol: QUIC, expectedProtocol: QUIC,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel quic and http2 disabled", name: "named tunnel quic and http2 disabled",
protocol: "auto", protocol: "auto",
expectedProtocol: H2mux, expectedProtocol: H2mux,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel quic disabled", name: "named tunnel quic disabled",
@ -104,21 +104,21 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: true, hasFallback: true,
expectedFallback: H2mux, expectedFallback: H2mux,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel auto all http2 disabled", name: "named tunnel auto all http2 disabled",
protocol: "auto", protocol: "auto",
expectedProtocol: H2mux, expectedProtocol: H2mux,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel auto to h2mux", name: "named tunnel auto to h2mux",
protocol: "auto", protocol: "auto",
expectedProtocol: H2mux, expectedProtocol: H2mux,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel auto to http2", name: "named tunnel auto to http2",
@ -127,7 +127,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: true, hasFallback: true,
expectedFallback: H2mux, expectedFallback: H2mux,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "named tunnel auto to quic", name: "named tunnel auto to quic",
@ -136,7 +136,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: true, hasFallback: true,
expectedFallback: HTTP2, expectedFallback: HTTP2,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "warp routing requesting h2mux", name: "warp routing requesting h2mux",
@ -145,7 +145,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: false, hasFallback: false,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
warpRoutingEnabled: true, warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "warp routing requesting h2mux picks HTTP2 even if http2 percent is -1", name: "warp routing requesting h2mux picks HTTP2 even if http2 percent is -1",
@ -154,7 +154,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: false, hasFallback: false,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
warpRoutingEnabled: true, warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "warp routing http2", name: "warp routing http2",
@ -163,7 +163,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: false, hasFallback: false,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
warpRoutingEnabled: true, warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "warp routing quic", name: "warp routing quic",
@ -173,7 +173,7 @@ func TestNewProtocolSelector(t *testing.T) {
expectedFallback: HTTP2Warp, expectedFallback: HTTP2Warp,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
warpRoutingEnabled: true, warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "warp routing auto", name: "warp routing auto",
@ -182,7 +182,7 @@ func TestNewProtocolSelector(t *testing.T) {
hasFallback: false, hasFallback: false,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
warpRoutingEnabled: true, warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
name: "warp routing auto- quic", name: "warp routing auto- quic",
@ -192,7 +192,7 @@ func TestNewProtocolSelector(t *testing.T) {
expectedFallback: HTTP2Warp, expectedFallback: HTTP2Warp,
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
warpRoutingEnabled: true, warpRoutingEnabled: true,
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
}, },
{ {
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error // None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
@ -204,14 +204,14 @@ func TestNewProtocolSelector(t *testing.T) {
name: "named tunnel unknown protocol", name: "named tunnel unknown protocol",
protocol: "unknown", protocol: "unknown",
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}), fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
wantErr: true, wantErr: true,
}, },
{ {
name: "named tunnel fetch error", name: "named tunnel fetch error",
protocol: "auto", protocol: "auto",
fetchFunc: mockFetcher(true), fetchFunc: mockFetcher(true),
namedTunnelConfig: testNamedTunnelConfig, namedTunnelConfig: testNamedTunnelProperties,
expectedProtocol: HTTP2, expectedProtocol: HTTP2,
wantErr: false, wantErr: false,
}, },
@ -237,7 +237,7 @@ func TestNewProtocolSelector(t *testing.T) {
func TestAutoProtocolSelectorRefresh(t *testing.T) { func TestAutoProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{} fetcher := dynamicMockFetcher{}
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log) selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, H2mux, selector.Current()) assert.Equal(t, H2mux, selector.Current())
@ -267,7 +267,7 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) { func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
fetcher := dynamicMockFetcher{} fetcher := dynamicMockFetcher{}
// Since the user chooses http2 on purpose, we always stick to it. // Since the user chooses http2 on purpose, we always stick to it.
selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log) selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), testNoTTL, &log)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, HTTP2, selector.Current()) assert.Equal(t, HTTP2, selector.Current())
@ -297,7 +297,7 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
func TestProtocolSelectorRefreshTTL(t *testing.T) { func TestProtocolSelectorRefreshTTL(t *testing.T) {
fetcher := dynamicMockFetcher{} fetcher := dynamicMockFetcher{}
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}} fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}}
selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), time.Hour, &log) selector, err := NewProtocolSelector("auto", noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), time.Hour, &log)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, QUIC, selector.Current()) assert.Equal(t, QUIC, selector.Current())

View File

@ -36,7 +36,7 @@ const (
type QUICConnection struct { type QUICConnection struct {
session quic.Session session quic.Session
logger *zerolog.Logger logger *zerolog.Logger
httpProxy OriginProxy orchestrator Orchestrator
sessionManager datagramsession.Manager sessionManager datagramsession.Manager
controlStreamHandler ControlStreamHandler controlStreamHandler ControlStreamHandler
connOptions *tunnelpogs.ConnectionOptions connOptions *tunnelpogs.ConnectionOptions
@ -47,7 +47,7 @@ func NewQUICConnection(
quicConfig *quic.Config, quicConfig *quic.Config,
edgeAddr net.Addr, edgeAddr net.Addr,
tlsConfig *tls.Config, tlsConfig *tls.Config,
httpProxy OriginProxy, orchestrator Orchestrator,
connOptions *tunnelpogs.ConnectionOptions, connOptions *tunnelpogs.ConnectionOptions,
controlStreamHandler ControlStreamHandler, controlStreamHandler ControlStreamHandler,
logger *zerolog.Logger, logger *zerolog.Logger,
@ -66,7 +66,7 @@ func NewQUICConnection(
return &QUICConnection{ return &QUICConnection{
session: session, session: session,
httpProxy: httpProxy, orchestrator: orchestrator,
logger: logger, logger: logger,
sessionManager: sessionManager, sessionManager: sessionManager,
controlStreamHandler: controlStreamHandler, controlStreamHandler: controlStreamHandler,
@ -122,7 +122,7 @@ func (q *QUICConnection) serveControlStream(ctx context.Context, controlStream q
func (q *QUICConnection) acceptStream(ctx context.Context) error { func (q *QUICConnection) acceptStream(ctx context.Context) error {
defer q.Close() defer q.Close()
for { for {
stream, err := q.session.AcceptStream(ctx) quicStream, err := q.session.AcceptStream(ctx)
if err != nil { if err != nil {
// context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional. // context.Canceled is usually a user ctrl+c. We don't want to log an error here as it's intentional.
if errors.Is(err, context.Canceled) || q.controlStreamHandler.IsStopped() { if errors.Is(err, context.Canceled) || q.controlStreamHandler.IsStopped() {
@ -131,7 +131,9 @@ func (q *QUICConnection) acceptStream(ctx context.Context) error {
return fmt.Errorf("failed to accept QUIC stream: %w", err) return fmt.Errorf("failed to accept QUIC stream: %w", err)
} }
go func() { go func() {
stream := quicpogs.NewSafeStreamCloser(quicStream)
defer stream.Close() defer stream.Close()
if err = q.handleStream(stream); err != nil { if err = q.handleStream(stream); err != nil {
q.logger.Err(err).Msg("Failed to handle QUIC stream") q.logger.Err(err).Msg("Failed to handle QUIC stream")
} }
@ -144,7 +146,7 @@ func (q *QUICConnection) Close() {
q.session.CloseWithError(0, "") q.session.CloseWithError(0, "")
} }
func (q *QUICConnection) handleStream(stream quic.Stream) error { func (q *QUICConnection) handleStream(stream io.ReadWriteCloser) error {
signature, err := quicpogs.DetermineProtocol(stream) signature, err := quicpogs.DetermineProtocol(stream)
if err != nil { if err != nil {
return err return err
@ -173,6 +175,10 @@ func (q *QUICConnection) handleDataStream(stream *quicpogs.RequestServerStream)
return err return err
} }
originProxy, err := q.orchestrator.GetOriginProxy()
if err != nil {
return err
}
switch connectRequest.Type { switch connectRequest.Type {
case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket: case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket:
req, err := buildHTTPRequest(connectRequest, stream) req, err := buildHTTPRequest(connectRequest, stream)
@ -181,16 +187,16 @@ func (q *QUICConnection) handleDataStream(stream *quicpogs.RequestServerStream)
} }
w := newHTTPResponseAdapter(stream) w := newHTTPResponseAdapter(stream)
return q.httpProxy.ProxyHTTP(w, req, connectRequest.Type == quicpogs.ConnectionTypeWebsocket) return originProxy.ProxyHTTP(w, req, connectRequest.Type == quicpogs.ConnectionTypeWebsocket)
case quicpogs.ConnectionTypeTCP: case quicpogs.ConnectionTypeTCP:
rwa := &streamReadWriteAcker{stream} rwa := &streamReadWriteAcker{stream}
return q.httpProxy.ProxyTCP(context.Background(), rwa, &TCPRequest{Dest: connectRequest.Dest}) return originProxy.ProxyTCP(context.Background(), rwa, &TCPRequest{Dest: connectRequest.Dest})
} }
return nil return nil
} }
func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) error { func (q *QUICConnection) handleRPCStream(rpcStream *quicpogs.RPCServerStream) error {
return rpcStream.Serve(q, q.logger) return rpcStream.Serve(q, q, q.logger)
} }
// RegisterUdpSession is the RPC method invoked by edge to register and run a session // RegisterUdpSession is the RPC method invoked by edge to register and run a session
@ -258,6 +264,11 @@ func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uui
return q.sessionManager.UnregisterSession(ctx, sessionID, message, true) return q.sessionManager.UnregisterSession(ctx, sessionID, message, true)
} }
// UpdateConfiguration is the RPC method invoked by edge when there is a new configuration
func (q *QUICConnection) UpdateConfiguration(ctx context.Context, version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
return q.orchestrator.UpdateConfig(version, config)
}
// streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to // streamReadWriteAcker is a light wrapper over QUIC streams with a callback to send response back to
// the client. // the client.
type streamReadWriteAcker struct { type streamReadWriteAcker struct {

View File

@ -3,14 +3,9 @@ package connection
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/rand"
"crypto/rsa"
"crypto/tls" "crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
"io" "io"
"math/big"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -33,7 +28,7 @@ import (
) )
var ( var (
testTLSServerConfig = generateTLSConfig() testTLSServerConfig = quicpogs.GenerateTLSConfig()
testQUICConfig = &quic.Config{ testQUICConfig = &quic.Config{
KeepAlive: true, KeepAlive: true,
EnableDatagrams: true, EnableDatagrams: true,
@ -52,7 +47,7 @@ func TestQUICServer(t *testing.T) {
// This is simply a sample websocket frame message. // This is simply a sample websocket frame message.
wsBuf := &bytes.Buffer{} wsBuf := &bytes.Buffer{}
wsutil.WriteClientText(wsBuf, []byte("Hello")) wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
var tests = []struct { var tests = []struct {
desc string desc string
@ -84,7 +79,7 @@ func TestQUICServer(t *testing.T) {
}, },
{ {
desc: "test http body request streaming", desc: "test http body request streaming",
dest: "/echo_body", dest: "/slow_echo_body",
connectionType: quicpogs.ConnectionTypeHTTP, connectionType: quicpogs.ConnectionTypeHTTP,
metadata: []quicpogs.Metadata{ metadata: []quicpogs.Metadata{
{ {
@ -109,7 +104,7 @@ func TestQUICServer(t *testing.T) {
}, },
{ {
desc: "test ws proxy", desc: "test ws proxy",
dest: "/ok", dest: "/ws/echo",
connectionType: quicpogs.ConnectionTypeWebsocket, connectionType: quicpogs.ConnectionTypeWebsocket,
metadata: []quicpogs.Metadata{ metadata: []quicpogs.Metadata{
{ {
@ -130,7 +125,7 @@ func TestQUICServer(t *testing.T) {
}, },
}, },
message: wsBuf.Bytes(), message: wsBuf.Bytes(),
expectedResponse: []byte{0x81, 0x5, 0x48, 0x65, 0x6c, 0x6c, 0x6f}, expectedResponse: []byte{0x82, 0x5, 0x48, 0x65, 0x6c, 0x6c, 0x6f},
}, },
{ {
desc: "test tcp proxy", desc: "test tcp proxy",
@ -195,8 +190,9 @@ func quicServer(
session, err := earlyListener.Accept(ctx) session, err := earlyListener.Accept(ctx)
require.NoError(t, err) require.NoError(t, err)
stream, err := session.OpenStreamSync(context.Background()) quicStream, err := session.OpenStreamSync(context.Background())
require.NoError(t, err) require.NoError(t, err)
stream := quicpogs.NewSafeStreamCloser(quicStream)
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream} reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...) err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
@ -207,42 +203,20 @@ func quicServer(
if message != nil { if message != nil {
// ALPN successful. Write data. // ALPN successful. Write data.
_, err := stream.Write([]byte(message)) _, err := stream.Write(message)
require.NoError(t, err) require.NoError(t, err)
} }
response := make([]byte, len(expectedResponse)) response := make([]byte, len(expectedResponse))
stream.Read(response) _, err = stream.Read(response)
require.NoError(t, err) if err != io.EOF {
require.NoError(t, err)
}
// For now it is an echo server. Verify if the same data is returned. // For now it is an echo server. Verify if the same data is returned.
assert.Equal(t, expectedResponse, response) assert.Equal(t, expectedResponse, response)
} }
// Setup a bare-bones TLS config for the server
func generateTLSConfig() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"argotunnel"},
}
}
type mockOriginProxyWithRequest struct{} type mockOriginProxyWithRequest struct{}
func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Request, isWebsocket bool) error { func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Request, isWebsocket bool) error {
@ -259,11 +233,14 @@ func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Reque
} }
if isWebsocket { if isWebsocket {
return wsEndpoint(w, r) return wsEchoEndpoint(w, r)
} }
switch r.URL.Path { switch r.URL.Path {
case "/ok": case "/ok":
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK))) originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
case "/slow_echo_body":
time.Sleep(5)
fallthrough
case "/echo_body": case "/echo_body":
resp := &http.Response{ resp := &http.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
@ -583,12 +560,12 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
if closeType != closedByRemote { if closeType != closedByRemote {
// Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote // Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote
unregisterFromEdgeChan := make(chan struct{}) unregisterFromEdgeChan := make(chan struct{})
rpcServer := &mockSessionRPCServer{ sessionRPCServer := &mockSessionRPCServer{
sessionID: sessionID, sessionID: sessionID,
unregisterReason: expectedReason, unregisterReason: expectedReason,
calledUnregisterChan: unregisterFromEdgeChan, calledUnregisterChan: unregisterFromEdgeChan,
} }
go runMockSessionRPCServer(ctx, edgeQUICSession, rpcServer, t) go runRPCServer(ctx, edgeQUICSession, sessionRPCServer, nil, t)
<-unregisterFromEdgeChan <-unregisterFromEdgeChan
} }
@ -604,7 +581,7 @@ const (
closedByTimeout closedByTimeout
) )
func runMockSessionRPCServer(ctx context.Context, session quic.Session, rpcServer *mockSessionRPCServer, t *testing.T) { func runRPCServer(ctx context.Context, session quic.Session, sessionRPCServer tunnelpogs.SessionManager, configRPCServer tunnelpogs.ConfigurationManager, t *testing.T) {
stream, err := session.AcceptStream(ctx) stream, err := session.AcceptStream(ctx)
require.NoError(t, err) require.NoError(t, err)
@ -619,7 +596,7 @@ func runMockSessionRPCServer(ctx context.Context, session quic.Session, rpcServe
assert.NoError(t, err) assert.NoError(t, err)
log := zerolog.New(os.Stdout) log := zerolog.New(os.Stdout)
err = rpcServerStream.Serve(rpcServer, &log) err = rpcServerStream.Serve(sessionRPCServer, configRPCServer, &log)
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -641,7 +618,6 @@ func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionI
return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason) return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason)
} }
close(s.calledUnregisterChan) close(s.calledUnregisterChan)
fmt.Println("unregister from edge")
return nil return nil
} }
@ -651,13 +627,12 @@ func testQUICConnection(udpListenerAddr net.Addr, t *testing.T) *QUICConnection
NextProtos: []string{"argotunnel"}, NextProtos: []string{"argotunnel"},
} }
// Start a mock httpProxy // Start a mock httpProxy
originProxy := &mockOriginProxyWithRequest{}
log := zerolog.New(os.Stdout) log := zerolog.New(os.Stdout)
qc, err := NewQUICConnection( qc, err := NewQUICConnection(
testQUICConfig, testQUICConfig,
udpListenerAddr, udpListenerAddr,
tlsClientConfig, tlsClientConfig,
originProxy, &mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},
&tunnelpogs.ConnectionOptions{}, &tunnelpogs.ConnectionOptions{},
fakeControlStream{}, fakeControlStream{},
&log, &log,

View File

@ -37,7 +37,7 @@ func NewTunnelServerClient(
} }
} }
func (tsc *tunnelServerClient) Authenticate(ctx context.Context, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) (tunnelpogs.AuthOutcome, error) { func (tsc *tunnelServerClient) Authenticate(ctx context.Context, classicTunnel *ClassicTunnelProperties, registrationOptions *tunnelpogs.RegistrationOptions) (tunnelpogs.AuthOutcome, error) {
authResp, err := tsc.client.Authenticate(ctx, classicTunnel.OriginCert, classicTunnel.Hostname, registrationOptions) authResp, err := tsc.client.Authenticate(ctx, classicTunnel.OriginCert, classicTunnel.Hostname, registrationOptions)
if err != nil { if err != nil {
return nil, err return nil, err
@ -54,7 +54,7 @@ func (tsc *tunnelServerClient) Close() {
type NamedTunnelRPCClient interface { type NamedTunnelRPCClient interface {
RegisterConnection( RegisterConnection(
c context.Context, c context.Context,
config *NamedTunnelConfig, config *NamedTunnelProperties,
options *tunnelpogs.ConnectionOptions, options *tunnelpogs.ConnectionOptions,
connIndex uint8, connIndex uint8,
observer *Observer, observer *Observer,
@ -86,15 +86,15 @@ func newRegistrationRPCClient(
func (rsc *registrationServerClient) RegisterConnection( func (rsc *registrationServerClient) RegisterConnection(
ctx context.Context, ctx context.Context,
config *NamedTunnelConfig, properties *NamedTunnelProperties,
options *tunnelpogs.ConnectionOptions, options *tunnelpogs.ConnectionOptions,
connIndex uint8, connIndex uint8,
observer *Observer, observer *Observer,
) error { ) error {
conn, err := rsc.client.RegisterConnection( conn, err := rsc.client.RegisterConnection(
ctx, ctx,
config.Credentials.Auth(), properties.Credentials.Auth(),
config.Credentials.TunnelID, properties.Credentials.TunnelID,
connIndex, connIndex,
options, options,
) )
@ -137,7 +137,7 @@ const (
authenticate rpcName = " authenticate" authenticate rpcName = " authenticate"
) )
func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter CredentialManager, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) error { func (h *h2muxConnection) registerTunnel(ctx context.Context, credentialSetter CredentialManager, classicTunnel *ClassicTunnelProperties, registrationOptions *tunnelpogs.RegistrationOptions) error {
h.observer.sendRegisteringEvent(registrationOptions.ConnectionID) h.observer.sendRegisteringEvent(registrationOptions.ConnectionID)
stream, err := h.newRPCStream(ctx, register) stream, err := h.newRPCStream(ctx, register)
@ -174,7 +174,7 @@ type CredentialManager interface {
func (h *h2muxConnection) processRegistrationSuccess( func (h *h2muxConnection) processRegistrationSuccess(
registration *tunnelpogs.TunnelRegistration, registration *tunnelpogs.TunnelRegistration,
name rpcName, name rpcName,
credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig, credentialManager CredentialManager, classicTunnel *ClassicTunnelProperties,
) error { ) error {
for _, logLine := range registration.LogLines { for _, logLine := range registration.LogLines {
h.observer.log.Info().Msg(logLine) h.observer.log.Info().Msg(logLine)
@ -205,7 +205,7 @@ func (h *h2muxConnection) processRegisterTunnelError(err tunnelpogs.TunnelRegist
} }
} }
func (h *h2muxConnection) reconnectTunnel(ctx context.Context, credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig, registrationOptions *tunnelpogs.RegistrationOptions) error { func (h *h2muxConnection) reconnectTunnel(ctx context.Context, credentialManager CredentialManager, classicTunnel *ClassicTunnelProperties, registrationOptions *tunnelpogs.RegistrationOptions) error {
token, err := credentialManager.ReconnectToken() token, err := credentialManager.ReconnectToken()
if err != nil { if err != nil {
return err return err
@ -264,7 +264,7 @@ func (h *h2muxConnection) logServerInfo(ctx context.Context, rpcClient *tunnelSe
func (h *h2muxConnection) registerNamedTunnel( func (h *h2muxConnection) registerNamedTunnel(
ctx context.Context, ctx context.Context,
namedTunnel *NamedTunnelConfig, namedTunnel *NamedTunnelProperties,
connOptions *tunnelpogs.ConnectionOptions, connOptions *tunnelpogs.ConnectionOptions,
) error { ) error {
stream, err := h.newRPCStream(ctx, register) stream, err := h.newRPCStream(ctx, register)
@ -283,7 +283,7 @@ func (h *h2muxConnection) registerNamedTunnel(
func (h *h2muxConnection) unregister(isNamedTunnel bool) { func (h *h2muxConnection) unregister(isNamedTunnel bool) {
h.observer.sendUnregisteringEvent(h.connIndex) h.observer.sendUnregisteringEvent(h.connIndex)
unregisterCtx, cancel := context.WithTimeout(context.Background(), h.config.GracePeriod) unregisterCtx, cancel := context.WithTimeout(context.Background(), h.gracePeriod)
defer cancel() defer cancel()
stream, err := h.newRPCStream(unregisterCtx, unregister) stream, err := h.newRPCStream(unregisterCtx, unregister)
@ -296,13 +296,13 @@ func (h *h2muxConnection) unregister(isNamedTunnel bool) {
rpcClient := h.newRPCClientFunc(unregisterCtx, stream, h.observer.log) rpcClient := h.newRPCClientFunc(unregisterCtx, stream, h.observer.log)
defer rpcClient.Close() defer rpcClient.Close()
rpcClient.GracefulShutdown(unregisterCtx, h.config.GracePeriod) rpcClient.GracefulShutdown(unregisterCtx, h.gracePeriod)
} else { } else {
rpcClient := NewTunnelServerClient(unregisterCtx, stream, h.observer.log) rpcClient := NewTunnelServerClient(unregisterCtx, stream, h.observer.log)
defer rpcClient.Close() defer rpcClient.Close()
// gracePeriod is encoded in int64 using capnproto // gracePeriod is encoded in int64 using capnproto
_ = rpcClient.client.UnregisterTunnel(unregisterCtx, h.config.GracePeriod.Nanoseconds()) _ = rpcClient.client.UnregisterTunnel(unregisterCtx, h.gracePeriod.Nanoseconds())
} }
h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unregistered tunnel connection") h.observer.log.Info().Uint8(LogFieldConnIndex, h.connIndex).Msg("Unregistered tunnel connection")

View File

@ -1,6 +1,7 @@
package ingress package ingress
import ( import (
"encoding/json"
"time" "time"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@ -38,6 +39,34 @@ const (
socksProxy = "socks" socksProxy = "socks"
) )
// RemoteConfig models ingress settings that can be managed remotely, for example through the dashboard.
type RemoteConfig struct {
Ingress Ingress
WarpRouting config.WarpRoutingConfig
}
type remoteConfigJSON struct {
GlobalOriginRequest config.OriginRequestConfig `json:"originRequest"`
IngressRules []config.UnvalidatedIngressRule `json:"ingress"`
WarpRouting config.WarpRoutingConfig `json:"warp-routing"`
}
func (rc *RemoteConfig) UnmarshalJSON(b []byte) error {
var rawConfig remoteConfigJSON
if err := json.Unmarshal(b, &rawConfig); err != nil {
return err
}
ingress, err := validateIngress(rawConfig.IngressRules, originRequestFromConfig(rawConfig.GlobalOriginRequest))
if err != nil {
return err
}
rc.Ingress = ingress
rc.WarpRouting = rawConfig.WarpRouting
return nil
}
func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig { func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
var connectTimeout time.Duration = defaultConnectTimeout var connectTimeout time.Duration = defaultConnectTimeout
var tlsTimeout time.Duration = defaultTLSTimeout var tlsTimeout time.Duration = defaultTLSTimeout
@ -119,7 +148,7 @@ func originRequestFromSingeRule(c *cli.Context) OriginRequestConfig {
} }
} }
func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig { func originRequestFromConfig(c config.OriginRequestConfig) OriginRequestConfig {
out := OriginRequestConfig{ out := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout, ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout, TLSTimeout: defaultTLSTimeout,
@ -128,50 +157,58 @@ func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig {
KeepAliveTimeout: defaultKeepAliveTimeout, KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress, ProxyAddress: defaultProxyAddress,
} }
if y.ConnectTimeout != nil { if c.ConnectTimeout != nil {
out.ConnectTimeout = *y.ConnectTimeout out.ConnectTimeout = *c.ConnectTimeout
} }
if y.TLSTimeout != nil { if c.TLSTimeout != nil {
out.TLSTimeout = *y.TLSTimeout out.TLSTimeout = *c.TLSTimeout
} }
if y.TCPKeepAlive != nil { if c.TCPKeepAlive != nil {
out.TCPKeepAlive = *y.TCPKeepAlive out.TCPKeepAlive = *c.TCPKeepAlive
} }
if y.NoHappyEyeballs != nil { if c.NoHappyEyeballs != nil {
out.NoHappyEyeballs = *y.NoHappyEyeballs out.NoHappyEyeballs = *c.NoHappyEyeballs
} }
if y.KeepAliveConnections != nil { if c.KeepAliveConnections != nil {
out.KeepAliveConnections = *y.KeepAliveConnections out.KeepAliveConnections = *c.KeepAliveConnections
} }
if y.KeepAliveTimeout != nil { if c.KeepAliveTimeout != nil {
out.KeepAliveTimeout = *y.KeepAliveTimeout out.KeepAliveTimeout = *c.KeepAliveTimeout
} }
if y.HTTPHostHeader != nil { if c.HTTPHostHeader != nil {
out.HTTPHostHeader = *y.HTTPHostHeader out.HTTPHostHeader = *c.HTTPHostHeader
} }
if y.OriginServerName != nil { if c.OriginServerName != nil {
out.OriginServerName = *y.OriginServerName out.OriginServerName = *c.OriginServerName
} }
if y.CAPool != nil { if c.CAPool != nil {
out.CAPool = *y.CAPool out.CAPool = *c.CAPool
} }
if y.NoTLSVerify != nil { if c.NoTLSVerify != nil {
out.NoTLSVerify = *y.NoTLSVerify out.NoTLSVerify = *c.NoTLSVerify
} }
if y.DisableChunkedEncoding != nil { if c.DisableChunkedEncoding != nil {
out.DisableChunkedEncoding = *y.DisableChunkedEncoding out.DisableChunkedEncoding = *c.DisableChunkedEncoding
} }
if y.BastionMode != nil { if c.BastionMode != nil {
out.BastionMode = *y.BastionMode out.BastionMode = *c.BastionMode
} }
if y.ProxyAddress != nil { if c.ProxyAddress != nil {
out.ProxyAddress = *y.ProxyAddress out.ProxyAddress = *c.ProxyAddress
} }
if y.ProxyPort != nil { if c.ProxyPort != nil {
out.ProxyPort = *y.ProxyPort out.ProxyPort = *c.ProxyPort
} }
if y.ProxyType != nil { if c.ProxyType != nil {
out.ProxyType = *y.ProxyType out.ProxyType = *c.ProxyType
}
if len(c.IPRules) > 0 {
for _, r := range c.IPRules {
rule, err := ipaccess.NewRuleByCIDR(r.Prefix, r.Ports, r.Allow)
if err == nil {
out.IPRules = append(out.IPRules, rule)
}
}
} }
return out return out
} }
@ -188,10 +225,10 @@ type OriginRequestConfig struct {
TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"` TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"`
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback // HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
NoHappyEyeballs bool `yaml:"noHappyEyeballs"` NoHappyEyeballs bool `yaml:"noHappyEyeballs"`
// HTTP proxy maximum keepalive connection pool size
KeepAliveConnections int `yaml:"keepAliveConnections"`
// HTTP proxy timeout for closing an idle connection // HTTP proxy timeout for closing an idle connection
KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"` KeepAliveTimeout time.Duration `yaml:"keepAliveTimeout"`
// HTTP proxy maximum keepalive connection pool size
KeepAliveConnections int `yaml:"keepAliveConnections"`
// Sets the HTTP Host header for the local webserver. // Sets the HTTP Host header for the local webserver.
HTTPHostHeader string `yaml:"httpHostHeader"` HTTPHostHeader string `yaml:"httpHostHeader"`
// Hostname on the origin server certificate. // Hostname on the origin server certificate.
@ -308,6 +345,19 @@ func (defaults *OriginRequestConfig) setProxyType(overrides config.OriginRequest
} }
} }
func (defaults *OriginRequestConfig) setIPRules(overrides config.OriginRequestConfig) {
if val := overrides.IPRules; len(val) > 0 {
ipAccessRule := make([]ipaccess.Rule, len(overrides.IPRules))
for i, r := range overrides.IPRules {
rule, err := ipaccess.NewRuleByCIDR(r.Prefix, r.Ports, r.Allow)
if err == nil {
ipAccessRule[i] = rule
}
}
defaults.IPRules = ipAccessRule
}
}
// SetConfig gets config for the requests that cloudflared sends to origins. // SetConfig gets config for the requests that cloudflared sends to origins.
// Each field has a setter method which sets a value for the field by trying to find: // Each field has a setter method which sets a value for the field by trying to find:
// 1. The user config for this rule // 1. The user config for this rule
@ -332,5 +382,6 @@ func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfi
cfg.setProxyPort(overrides) cfg.setProxyPort(overrides)
cfg.setProxyAddress(overrides) cfg.setProxyAddress(overrides)
cfg.setProxyType(overrides) cfg.setProxyType(overrides)
cfg.setIPRules(overrides)
return cfg return cfg
} }

422
ingress/config_test.go Normal file
View File

@ -0,0 +1,422 @@
package ingress
import (
"encoding/json"
"flag"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
yaml "gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/config"
"github.com/cloudflare/cloudflared/ipaccess"
)
// Ensure that the nullable config from `config` package and the
// non-nullable config from `ingress` package have the same number of
// fields.
// This test ensures that programmers didn't add a new field to
// one struct and forget to add it to the other ;)
func TestCorrespondingFields(t *testing.T) {
require.Equal(
t,
CountFields(t, config.OriginRequestConfig{}),
CountFields(t, OriginRequestConfig{}),
)
}
func CountFields(t *testing.T, val interface{}) int {
b, err := yaml.Marshal(val)
require.NoError(t, err)
m := make(map[string]interface{}, 0)
err = yaml.Unmarshal(b, &m)
require.NoError(t, err)
return len(m)
}
func TestUnmarshalRemoteConfigOverridesGlobal(t *testing.T) {
rawConfig := []byte(`
{
"originRequest": {
"connectTimeout": 90,
"noHappyEyeballs": true
},
"ingress": [
{
"hostname": "jira.cfops.com",
"service": "http://192.16.19.1:80",
"originRequest": {
"noTLSVerify": true,
"connectTimeout": 10
}
},
{
"service": "http_status:404"
}
],
"warp-routing": {
"enabled": true
}
}
`)
var remoteConfig RemoteConfig
err := json.Unmarshal(rawConfig, &remoteConfig)
require.NoError(t, err)
require.True(t, remoteConfig.Ingress.Rules[0].Config.NoTLSVerify)
require.True(t, remoteConfig.Ingress.defaults.NoHappyEyeballs)
}
func TestOriginRequestConfigOverrides(t *testing.T) {
validate := func(ing Ingress) {
// Rule 0 didn't override anything, so it inherits the user-specified
// root-level configuration.
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: 1 * time.Minute,
TLSTimeout: 1 * time.Second,
TCPKeepAlive: 1 * time.Second,
NoHappyEyeballs: true,
KeepAliveTimeout: 1 * time.Second,
KeepAliveConnections: 1,
HTTPHostHeader: "abc",
OriginServerName: "a1",
CAPool: "/tmp/path0",
NoTLSVerify: true,
DisableChunkedEncoding: true,
BastionMode: true,
ProxyAddress: "127.1.2.3",
ProxyPort: uint(100),
ProxyType: "socks5",
IPRules: []ipaccess.Rule{
newIPRule(t, "10.0.0.0/8", []int{80, 8080}, false),
newIPRule(t, "fc00::/7", []int{443, 4443}, true),
},
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all the root-level config.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
TCPKeepAlive: 2 * time.Second,
NoHappyEyeballs: false,
KeepAliveTimeout: 2 * time.Second,
KeepAliveConnections: 2,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
IPRules: []ipaccess.Rule{
newIPRule(t, "10.0.0.0/16", []int{3000, 3030}, false),
newIPRule(t, "192.16.0.0/24", []int{5000, 5050}, true),
},
}
require.Equal(t, expected1, actual1)
}
rulesYAML := `
originRequest:
connectTimeout: 1m
tlsTimeout: 1s
noHappyEyeballs: true
tcpKeepAlive: 1s
keepAliveConnections: 1
keepAliveTimeout: 1s
httpHostHeader: abc
originServerName: a1
caPool: /tmp/path0
noTLSVerify: true
disableChunkedEncoding: true
bastionMode: True
proxyAddress: 127.1.2.3
proxyPort: 100
proxyType: socks5
ipRules:
- prefix: "10.0.0.0/8"
ports:
- 80
- 8080
allow: false
- prefix: "fc00::/7"
ports:
- 443
- 4443
allow: true
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
ipRules:
- prefix: "10.0.0.0/16"
ports:
- 3000
- 3030
allow: false
- prefix: "192.16.0.0/24"
ports:
- 5000
- 5050
allow: true
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
require.NoError(t, err)
validate(ing)
rawConfig := []byte(`
{
"originRequest": {
"connectTimeout": 60000000000,
"tlsTimeout": 1000000000,
"noHappyEyeballs": true,
"tcpKeepAlive": 1000000000,
"keepAliveConnections": 1,
"keepAliveTimeout": 1000000000,
"httpHostHeader": "abc",
"originServerName": "a1",
"caPool": "/tmp/path0",
"noTLSVerify": true,
"disableChunkedEncoding": true,
"bastionMode": true,
"proxyAddress": "127.1.2.3",
"proxyPort": 100,
"proxyType": "socks5",
"ipRules": [
{
"prefix": "10.0.0.0/8",
"ports": [80, 8080],
"allow": false
},
{
"prefix": "fc00::/7",
"ports": [443, 4443],
"allow": true
}
]
},
"ingress": [
{
"hostname": "tun.example.com",
"service": "https://localhost:8000"
},
{
"hostname": "*",
"service": "https://localhost:8001",
"originRequest": {
"connectTimeout": 120000000000,
"tlsTimeout": 2000000000,
"noHappyEyeballs": false,
"tcpKeepAlive": 2000000000,
"keepAliveConnections": 2,
"keepAliveTimeout": 2000000000,
"httpHostHeader": "def",
"originServerName": "b2",
"caPool": "/tmp/path1",
"noTLSVerify": false,
"disableChunkedEncoding": false,
"bastionMode": false,
"proxyAddress": "interface",
"proxyPort": 200,
"proxyType": "",
"ipRules": [
{
"prefix": "10.0.0.0/16",
"ports": [3000, 3030],
"allow": false
},
{
"prefix": "192.16.0.0/24",
"ports": [5000, 5050],
"allow": true
}
]
}
}
],
"warp-routing": {
"enabled": true
}
}
`)
var remoteConfig RemoteConfig
err = json.Unmarshal(rawConfig, &remoteConfig)
require.NoError(t, err)
validate(remoteConfig.Ingress)
}
func TestOriginRequestConfigDefaults(t *testing.T) {
validate := func(ing Ingress) {
// Rule 0 didn't override anything, so it inherits the cloudflared defaults
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all defaults.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
TCPKeepAlive: 2 * time.Second,
NoHappyEyeballs: false,
KeepAliveTimeout: 2 * time.Second,
KeepAliveConnections: 2,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
IPRules: []ipaccess.Rule{
newIPRule(t, "10.0.0.0/16", []int{3000, 3030}, false),
newIPRule(t, "192.16.0.0/24", []int{5000, 5050}, true),
},
}
require.Equal(t, expected1, actual1)
}
rulesYAML := `
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
ipRules:
- prefix: "10.0.0.0/16"
ports:
- 3000
- 3030
allow: false
- prefix: "192.16.0.0/24"
ports:
- 5000
- 5050
allow: true
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
validate(ing)
rawConfig := []byte(`
{
"ingress": [
{
"hostname": "tun.example.com",
"service": "https://localhost:8000"
},
{
"hostname": "*",
"service": "https://localhost:8001",
"originRequest": {
"connectTimeout": 120000000000,
"tlsTimeout": 2000000000,
"noHappyEyeballs": false,
"tcpKeepAlive": 2000000000,
"keepAliveConnections": 2,
"keepAliveTimeout": 2000000000,
"httpHostHeader": "def",
"originServerName": "b2",
"caPool": "/tmp/path1",
"noTLSVerify": false,
"disableChunkedEncoding": false,
"bastionMode": false,
"proxyAddress": "interface",
"proxyPort": 200,
"proxyType": "",
"ipRules": [
{
"prefix": "10.0.0.0/16",
"ports": [3000, 3030],
"allow": false
},
{
"prefix": "192.16.0.0/24",
"ports": [5000, 5050],
"allow": true
}
]
}
}
]
}
`)
var remoteConfig RemoteConfig
err = json.Unmarshal(rawConfig, &remoteConfig)
require.NoError(t, err)
validate(remoteConfig.Ingress)
}
func TestDefaultConfigFromCLI(t *testing.T) {
set := flag.NewFlagSet("contrive", 0)
c := cli.NewContext(nil, set, nil)
expected := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
actual := originRequestFromSingeRule(c)
require.Equal(t, expected, actual)
}
func newIPRule(t *testing.T, prefix string, ports []int, allow bool) ipaccess.Rule {
rule, err := ipaccess.NewRuleByCIDR(&prefix, ports, allow)
require.NoError(t, err)
return rule
}

View File

@ -7,7 +7,6 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"sync"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/rs/zerolog" "github.com/rs/zerolog"
@ -145,13 +144,11 @@ func (ing Ingress) IsSingleRule() bool {
// StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World. // StartOrigins will start any origin services managed by cloudflared, e.g. proxy servers or Hello World.
func (ing Ingress) StartOrigins( func (ing Ingress) StartOrigins(
wg *sync.WaitGroup,
log *zerolog.Logger, log *zerolog.Logger,
shutdownC <-chan struct{}, shutdownC <-chan struct{},
errC chan error,
) error { ) error {
for _, rule := range ing.Rules { for _, rule := range ing.Rules {
if err := rule.Service.start(wg, log, shutdownC, errC, rule.Config); err != nil { if err := rule.Service.start(log, shutdownC, rule.Config); err != nil {
return errors.Wrapf(err, "Error starting local service %s", rule.Service) return errors.Wrapf(err, "Error starting local service %s", rule.Service)
} }
} }
@ -163,7 +160,7 @@ func (ing Ingress) CatchAll() *Rule {
return &ing.Rules[len(ing.Rules)-1] return &ing.Rules[len(ing.Rules)-1]
} }
func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) { func validateIngress(ingress []config.UnvalidatedIngressRule, defaults OriginRequestConfig) (Ingress, error) {
rules := make([]Rule, len(ingress)) rules := make([]Rule, len(ingress))
for i, r := range ingress { for i, r := range ingress {
cfg := setConfig(defaults, r.OriginRequest) cfg := setConfig(defaults, r.OriginRequest)
@ -290,7 +287,7 @@ func ParseIngress(conf *config.Configuration) (Ingress, error) {
if len(conf.Ingress) == 0 { if len(conf.Ingress) == 0 {
return Ingress{}, ErrNoIngressRules return Ingress{}, ErrNoIngressRules
} }
return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest)) return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest))
} }
func isHTTPService(url *url.URL) bool { func isHTTPService(url *url.URL) bool {

View File

@ -34,7 +34,7 @@ func Test_parseIngress(t *testing.T) {
localhost8000 := MustParseURL(t, "https://localhost:8000") localhost8000 := MustParseURL(t, "https://localhost:8000")
localhost8001 := MustParseURL(t, "https://localhost:8001") localhost8001 := MustParseURL(t, "https://localhost:8001")
fourOhFour := newStatusCode(404) fourOhFour := newStatusCode(404)
defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{}) defaultConfig := setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{})
require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections) require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections)
tr := true tr := true
type args struct { type args struct {
@ -324,7 +324,17 @@ ingress:
{ {
Hostname: "socks.foo.com", Hostname: "socks.foo.com",
Service: newSocksProxyOverWSService(accessPolicy()), Service: newSocksProxyOverWSService(accessPolicy()),
Config: defaultConfig, Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{IPRules: []config.IngressIPRule{
{
Prefix: ipRulePrefix("1.1.1.0/24"),
Ports: []int{80, 443},
Allow: true,
},
{
Prefix: ipRulePrefix("0.0.0.0/0"),
Allow: false,
},
}}),
}, },
{ {
Service: &fourOhFour, Service: &fourOhFour,
@ -345,7 +355,7 @@ ingress:
{ {
Hostname: "bastion.foo.com", Hostname: "bastion.foo.com",
Service: newBastionService(), Service: newBastionService(),
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
}, },
{ {
Service: &fourOhFour, Service: &fourOhFour,
@ -365,7 +375,7 @@ ingress:
{ {
Hostname: "bastion.foo.com", Hostname: "bastion.foo.com",
Service: newBastionService(), Service: newBastionService(),
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}), Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
}, },
{ {
Service: &fourOhFour, Service: &fourOhFour,
@ -397,6 +407,10 @@ ingress:
} }
} }
func ipRulePrefix(s string) *string {
return &s
}
func TestSingleOriginSetsConfig(t *testing.T) { func TestSingleOriginSetsConfig(t *testing.T) {
flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError) flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError)
flagSet.Bool("hello-world", true, "") flagSet.Bool("hello-world", true, "")

View File

@ -53,7 +53,7 @@ func (wc *tcpOverWSConnection) Stream(ctx context.Context, tunnelConn io.ReadWri
wc.streamHandler(wsConn, wc.conn, log) wc.streamHandler(wsConn, wc.conn, log)
cancel() cancel()
// Makes sure wsConn stops sending ping before terminating the stream // Makes sure wsConn stops sending ping before terminating the stream
wsConn.WaitForShutdown() wsConn.Close()
} }
func (wc *tcpOverWSConnection) Close() { func (wc *tcpOverWSConnection) Close() {
@ -73,21 +73,8 @@ func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.
socks.StreamNetHandler(wsConn, sp.accessPolicy, log) socks.StreamNetHandler(wsConn, sp.accessPolicy, log)
cancel() cancel()
// Makes sure wsConn stops sending ping before terminating the stream // Makes sure wsConn stops sending ping before terminating the stream
wsConn.WaitForShutdown() wsConn.Close()
} }
func (sp *socksProxyOverWSConnection) Close() { func (sp *socksProxyOverWSConnection) Close() {
} }
// wsProxyConnection represents a bidirectional stream for a websocket connection to the origin
type wsProxyConnection struct {
rwc io.ReadWriteCloser
}
func (conn *wsProxyConnection) Stream(ctx context.Context, tunnelConn io.ReadWriter, log *zerolog.Logger) {
websocket.Stream(tunnelConn, conn.rwc, log)
}
func (conn *wsProxyConnection) Close() {
conn.rwc.Close()
}

View File

@ -23,6 +23,7 @@ type StreamBasedOriginProxy interface {
} }
func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) { func (o *unixSocketPath) RoundTrip(req *http.Request) (*http.Response, error) {
req.URL.Scheme = "http"
return o.transport.RoundTrip(req) return o.transport.RoundTrip(req)
} }

View File

@ -8,7 +8,6 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"sync"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -132,10 +131,8 @@ func TestHTTPServiceHostHeaderOverride(t *testing.T) {
httpService := &httpService{ httpService := &httpService{
url: originURL, url: originURL,
} }
var wg sync.WaitGroup
shutdownC := make(chan struct{}) shutdownC := make(chan struct{})
errC := make(chan error) require.NoError(t, httpService.start(testLogger, shutdownC, cfg))
require.NoError(t, httpService.start(&wg, testLogger, shutdownC, errC, cfg))
req, err := http.NewRequest(http.MethodGet, originURL.String(), nil) req, err := http.NewRequest(http.MethodGet, originURL.String(), nil)
require.NoError(t, err) require.NoError(t, err)
@ -147,7 +144,46 @@ func TestHTTPServiceHostHeaderOverride(t *testing.T) {
respBody, err := ioutil.ReadAll(resp.Body) respBody, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, respBody, []byte(originURL.Host)) require.Equal(t, respBody, []byte(originURL.Host))
}
// TestHTTPServiceUsesIngressRuleScheme makes sure httpService uses scheme defined in ingress rule and not by eyeball request
func TestHTTPServiceUsesIngressRuleScheme(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
require.NotNil(t, r.TLS)
// Echo the X-Forwarded-Proto header for assertions
w.Write([]byte(r.Header.Get("X-Forwarded-Proto")))
}
origin := httptest.NewTLSServer(http.HandlerFunc(handler))
defer origin.Close()
originURL, err := url.Parse(origin.URL)
require.NoError(t, err)
require.Equal(t, "https", originURL.Scheme)
cfg := OriginRequestConfig{
NoTLSVerify: true,
}
httpService := &httpService{
url: originURL,
}
shutdownC := make(chan struct{})
require.NoError(t, httpService.start(testLogger, shutdownC, cfg))
// Tunnel uses scheme defined in the service field of the ingress rule, independent of the X-Forwarded-Proto header
protos := []string{"https", "http", "dne"}
for _, p := range protos {
req, err := http.NewRequest(http.MethodGet, originURL.String(), nil)
require.NoError(t, err)
req.Header.Add("X-Forwarded-Proto", p)
resp, err := httpService.RoundTrip(req)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
respBody, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, respBody, []byte(p))
}
} }
func tcpListenRoutine(listener net.Listener, closeChan chan struct{}) { func tcpListenRoutine(listener net.Listener, closeChan chan struct{}) {

View File

@ -1,203 +0,0 @@
package ingress
import (
"flag"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
yaml "gopkg.in/yaml.v2"
"github.com/cloudflare/cloudflared/config"
)
// Ensure that the nullable config from `config` package and the
// non-nullable config from `ingress` package have the same number of
// fields.
// This test ensures that programmers didn't add a new field to
// one struct and forget to add it to the other ;)
func TestCorrespondingFields(t *testing.T) {
require.Equal(
t,
CountFields(t, config.OriginRequestConfig{}),
CountFields(t, OriginRequestConfig{}),
)
}
func CountFields(t *testing.T, val interface{}) int {
b, err := yaml.Marshal(val)
require.NoError(t, err)
m := make(map[string]interface{}, 0)
err = yaml.Unmarshal(b, &m)
require.NoError(t, err)
return len(m)
}
func TestOriginRequestConfigOverrides(t *testing.T) {
rulesYAML := `
originRequest:
connectTimeout: 1m
tlsTimeout: 1s
noHappyEyeballs: true
tcpKeepAlive: 1s
keepAliveConnections: 1
keepAliveTimeout: 1s
httpHostHeader: abc
originServerName: a1
caPool: /tmp/path0
noTLSVerify: true
disableChunkedEncoding: true
bastionMode: True
proxyAddress: 127.1.2.3
proxyPort: 100
proxyType: socks5
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
// Rule 0 didn't override anything, so it inherits the user-specified
// root-level configuration.
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: 1 * time.Minute,
TLSTimeout: 1 * time.Second,
NoHappyEyeballs: true,
TCPKeepAlive: 1 * time.Second,
KeepAliveConnections: 1,
KeepAliveTimeout: 1 * time.Second,
HTTPHostHeader: "abc",
OriginServerName: "a1",
CAPool: "/tmp/path0",
NoTLSVerify: true,
DisableChunkedEncoding: true,
BastionMode: true,
ProxyAddress: "127.1.2.3",
ProxyPort: uint(100),
ProxyType: "socks5",
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all the root-level config.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
NoHappyEyeballs: false,
TCPKeepAlive: 2 * time.Second,
KeepAliveConnections: 2,
KeepAliveTimeout: 2 * time.Second,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
}
require.Equal(t, expected1, actual1)
}
func TestOriginRequestConfigDefaults(t *testing.T) {
rulesYAML := `
ingress:
- hostname: tun.example.com
service: https://localhost:8000
- hostname: "*"
service: https://localhost:8001
originRequest:
connectTimeout: 2m
tlsTimeout: 2s
noHappyEyeballs: false
tcpKeepAlive: 2s
keepAliveConnections: 2
keepAliveTimeout: 2s
httpHostHeader: def
originServerName: b2
caPool: /tmp/path1
noTLSVerify: false
disableChunkedEncoding: false
bastionMode: false
proxyAddress: interface
proxyPort: 200
proxyType: ""
`
ing, err := ParseIngress(MustReadIngress(rulesYAML))
if err != nil {
t.Error(err)
}
// Rule 0 didn't override anything, so it inherits the cloudflared defaults
actual0 := ing.Rules[0].Config
expected0 := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
require.Equal(t, expected0, actual0)
// Rule 1 overrode all defaults.
actual1 := ing.Rules[1].Config
expected1 := OriginRequestConfig{
ConnectTimeout: 2 * time.Minute,
TLSTimeout: 2 * time.Second,
NoHappyEyeballs: false,
TCPKeepAlive: 2 * time.Second,
KeepAliveConnections: 2,
KeepAliveTimeout: 2 * time.Second,
HTTPHostHeader: "def",
OriginServerName: "b2",
CAPool: "/tmp/path1",
NoTLSVerify: false,
DisableChunkedEncoding: false,
BastionMode: false,
ProxyAddress: "interface",
ProxyPort: uint(200),
ProxyType: "",
}
require.Equal(t, expected1, actual1)
}
func TestDefaultConfigFromCLI(t *testing.T) {
set := flag.NewFlagSet("contrive", 0)
c := cli.NewContext(nil, set, nil)
expected := OriginRequestConfig{
ConnectTimeout: defaultConnectTimeout,
TLSTimeout: defaultTLSTimeout,
TCPKeepAlive: defaultTCPKeepAlive,
KeepAliveConnections: defaultKeepAliveConnections,
KeepAliveTimeout: defaultKeepAliveTimeout,
ProxyAddress: defaultProxyAddress,
}
actual := originRequestFromSingeRule(c)
require.Equal(t, expected, actual)
}

View File

@ -8,7 +8,6 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"sync"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -20,13 +19,18 @@ import (
"github.com/cloudflare/cloudflared/tlsconfig" "github.com/cloudflare/cloudflared/tlsconfig"
) )
const (
HelloWorldService = "Hello World test origin"
)
// OriginService is something a tunnel can proxy traffic to. // OriginService is something a tunnel can proxy traffic to.
type OriginService interface { type OriginService interface {
String() string String() string
// Start the origin service if it's managed by cloudflared, e.g. proxy servers or Hello World. // Start the origin service if it's managed by cloudflared, e.g. proxy servers or Hello World.
// If it's not managed by cloudflared, this is a no-op because the user is responsible for // If it's not managed by cloudflared, this is a no-op because the user is responsible for
// starting the origin service. // starting the origin service.
start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error // Implementor of services managed by cloudflared should terminate the service if shutdownC is closed
start(log *zerolog.Logger, shutdownC <-chan struct{}, cfg OriginRequestConfig) error
} }
// unixSocketPath is an OriginService representing a unix socket (which accepts HTTP) // unixSocketPath is an OriginService representing a unix socket (which accepts HTTP)
@ -39,7 +43,7 @@ func (o *unixSocketPath) String() string {
return "unix socket: " + o.path return "unix socket: " + o.path
} }
func (o *unixSocketPath) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error { func (o *unixSocketPath) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg, log) transport, err := newHTTPTransport(o, cfg, log)
if err != nil { if err != nil {
return err return err
@ -54,7 +58,7 @@ type httpService struct {
transport *http.Transport transport *http.Transport
} }
func (o *httpService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error { func (o *httpService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
transport, err := newHTTPTransport(o, cfg, log) transport, err := newHTTPTransport(o, cfg, log)
if err != nil { if err != nil {
return err return err
@ -78,7 +82,7 @@ func (o *rawTCPService) String() string {
return o.name return o.name
} }
func (o *rawTCPService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error { func (o *rawTCPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
return nil return nil
} }
@ -139,7 +143,7 @@ func (o *tcpOverWSService) String() string {
return o.dest return o.dest
} }
func (o *tcpOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error { func (o *tcpOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
if cfg.ProxyType == socksProxy { if cfg.ProxyType == socksProxy {
o.streamHandler = socks.StreamHandler o.streamHandler = socks.StreamHandler
} else { } else {
@ -148,7 +152,7 @@ func (o *tcpOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdo
return nil return nil
} }
func (o *socksProxyOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error { func (o *socksProxyOverWSService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
return nil return nil
} }
@ -164,18 +168,16 @@ type helloWorld struct {
} }
func (o *helloWorld) String() string { func (o *helloWorld) String() string {
return "Hello World test origin" return HelloWorldService
} }
// Start starts a HelloWorld server and stores its address in the Service receiver. // Start starts a HelloWorld server and stores its address in the Service receiver.
func (o *helloWorld) start( func (o *helloWorld) start(
wg *sync.WaitGroup,
log *zerolog.Logger, log *zerolog.Logger,
shutdownC <-chan struct{}, shutdownC <-chan struct{},
errC chan error,
cfg OriginRequestConfig, cfg OriginRequestConfig,
) error { ) error {
if err := o.httpService.start(wg, log, shutdownC, errC, cfg); err != nil { if err := o.httpService.start(log, shutdownC, cfg); err != nil {
return err return err
} }
@ -183,11 +185,7 @@ func (o *helloWorld) start(
if err != nil { if err != nil {
return errors.Wrap(err, "Cannot start Hello World Server") return errors.Wrap(err, "Cannot start Hello World Server")
} }
wg.Add(1) go hello.StartHelloWorldServer(log, helloListener, shutdownC)
go func() {
defer wg.Done()
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
}()
o.server = helloListener o.server = helloListener
o.httpService.url = &url.URL{ o.httpService.url = &url.URL{
@ -218,10 +216,8 @@ func (o *statusCode) String() string {
} }
func (o *statusCode) start( func (o *statusCode) start(
wg *sync.WaitGroup,
log *zerolog.Logger, log *zerolog.Logger,
shutdownC <-chan struct{}, _ <-chan struct{},
errC chan error,
cfg OriginRequestConfig, cfg OriginRequestConfig,
) error { ) error {
return nil return nil
@ -296,6 +292,6 @@ func (mos MockOriginHTTPService) String() string {
return "MockOriginService" return "MockOriginService"
} }
func (mos MockOriginHTTPService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdownC <-chan struct{}, errC chan error, cfg OriginRequestConfig) error { func (mos MockOriginHTTPService) start(log *zerolog.Logger, _ <-chan struct{}, cfg OriginRequestConfig) error {
return nil return nil
} }

15
orchestration/config.go Normal file
View File

@ -0,0 +1,15 @@
package orchestration
import (
"github.com/cloudflare/cloudflared/ingress"
)
type newConfig struct {
ingress.RemoteConfig
// Add more fields when we support other settings in tunnel orchestration
}
type Config struct {
Ingress *ingress.Ingress
WarpRoutingEnabled bool
}

View File

@ -0,0 +1,158 @@
package orchestration
import (
"context"
"encoding/json"
"fmt"
"sync"
"sync/atomic"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/proxy"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
// Orchestrator manages configurations so they can be updatable during runtime
// properties are static, so it can be read without lock
// currentVersion and config are read/write infrequently, so their access are synchronized with RWMutex
// access to proxy is synchronized with atmoic.Value, because it uses copy-on-write to provide scalable frequently
// read when update is infrequent
type Orchestrator struct {
currentVersion int32
// Used by UpdateConfig to make sure one update at a time
lock sync.RWMutex
// Underlying value is proxy.Proxy, can be read without the lock, but still needs the lock to update
proxy atomic.Value
config *Config
tags []tunnelpogs.Tag
log *zerolog.Logger
// orchestrator must not handle any more updates after shutdownC is closed
shutdownC <-chan struct{}
// Closing proxyShutdownC will close the previous proxy
proxyShutdownC chan<- struct{}
}
func NewOrchestrator(ctx context.Context, config *Config, tags []tunnelpogs.Tag, log *zerolog.Logger) (*Orchestrator, error) {
o := &Orchestrator{
// Lowest possible version, any remote configuration will have version higher than this
currentVersion: 0,
config: config,
tags: tags,
log: log,
shutdownC: ctx.Done(),
}
if err := o.updateIngress(*config.Ingress, config.WarpRoutingEnabled); err != nil {
return nil, err
}
go o.waitToCloseLastProxy()
return o, nil
}
// Update creates a new proxy with the new ingress rules
func (o *Orchestrator) UpdateConfig(version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
o.lock.Lock()
defer o.lock.Unlock()
if o.currentVersion >= version {
o.log.Debug().
Int32("current_version", o.currentVersion).
Int32("received_version", version).
Msg("Current version is equal or newer than receivied version")
return &tunnelpogs.UpdateConfigurationResponse{
LastAppliedVersion: o.currentVersion,
}
}
var newConf newConfig
if err := json.Unmarshal(config, &newConf); err != nil {
o.log.Err(err).
Int32("version", version).
Str("config", string(config)).
Msgf("Failed to deserialize new configuration")
return &tunnelpogs.UpdateConfigurationResponse{
LastAppliedVersion: o.currentVersion,
Err: err,
}
}
if err := o.updateIngress(newConf.Ingress, newConf.WarpRouting.Enabled); err != nil {
o.log.Err(err).
Int32("version", version).
Str("config", string(config)).
Msgf("Failed to update ingress")
return &tunnelpogs.UpdateConfigurationResponse{
LastAppliedVersion: o.currentVersion,
Err: err,
}
}
o.currentVersion = version
o.log.Info().
Int32("version", version).
Str("config", string(config)).
Msg("Updated to new configuration")
return &tunnelpogs.UpdateConfigurationResponse{
LastAppliedVersion: o.currentVersion,
}
}
// The caller is responsible to make sure there is no concurrent access
func (o *Orchestrator) updateIngress(ingressRules ingress.Ingress, warpRoutingEnabled bool) error {
select {
case <-o.shutdownC:
return fmt.Errorf("cloudflared already shutdown")
default:
}
// Start new proxy before closing the ones from last version.
// The upside is we don't need to restart proxy from last version, which can fail
// The downside is new version might have ingress rule that require previous version to be shutdown first
// The downside is minimized because none of the ingress.OriginService implementation have that requirement
proxyShutdownC := make(chan struct{})
if err := ingressRules.StartOrigins(o.log, proxyShutdownC); err != nil {
return errors.Wrap(err, "failed to start origin")
}
newProxy := proxy.NewOriginProxy(ingressRules, warpRoutingEnabled, o.tags, o.log)
o.proxy.Store(newProxy)
o.config.Ingress = &ingressRules
o.config.WarpRoutingEnabled = warpRoutingEnabled
// If proxyShutdownC is nil, there is no previous running proxy
if o.proxyShutdownC != nil {
close(o.proxyShutdownC)
}
o.proxyShutdownC = proxyShutdownC
return nil
}
// GetOriginProxy returns an interface to proxy to origin. It satisfies connection.ConfigManager interface
func (o *Orchestrator) GetOriginProxy() (connection.OriginProxy, error) {
val := o.proxy.Load()
if val == nil {
err := fmt.Errorf("origin proxy not configured")
o.log.Error().Msg(err.Error())
return nil, err
}
proxy, ok := val.(*proxy.Proxy)
if !ok {
err := fmt.Errorf("origin proxy has unexpected value %+v", val)
o.log.Error().Msg(err.Error())
return nil, err
}
return proxy, nil
}
func (o *Orchestrator) waitToCloseLastProxy() {
<-o.shutdownC
o.lock.Lock()
defer o.lock.Unlock()
if o.proxyShutdownC != nil {
close(o.proxyShutdownC)
o.proxyShutdownC = nil
}
}

View File

@ -0,0 +1,686 @@
package orchestration
import (
"context"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
"github.com/gobwas/ws/wsutil"
gows "github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/cloudflare/cloudflared/connection"
"github.com/cloudflare/cloudflared/ingress"
"github.com/cloudflare/cloudflared/proxy"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
)
var (
testLogger = zerolog.Logger{}
testTags = []tunnelpogs.Tag{
{
Name: "package",
Value: "orchestration",
},
{
Name: "purpose",
Value: "test",
},
}
)
// TestUpdateConfiguration tests that
// - configurations can be deserialized
// - proxy can be updated
// - last applied version and error are returned
// - configurations can be deserialized
// - receiving an old version is noop
func TestUpdateConfiguration(t *testing.T) {
initConfig := &Config{
Ingress: &ingress.Ingress{},
WarpRoutingEnabled: false,
}
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, &testLogger)
require.NoError(t, err)
initOriginProxy, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
require.IsType(t, &proxy.Proxy{}, initOriginProxy)
configJSONV2 := []byte(`
{
"unknown_field": "not_deserialized",
"originRequest": {
"connectTimeout": 90000000000,
"noHappyEyeballs": true
},
"ingress": [
{
"hostname": "jira.tunnel.org",
"path": "^\/login",
"service": "http://192.16.19.1:443",
"originRequest": {
"noTLSVerify": true,
"connectTimeout": 10000000000
}
},
{
"hostname": "jira.tunnel.org",
"service": "http://172.32.20.6:80",
"originRequest": {
"noTLSVerify": true,
"connectTimeout": 30000000000
}
},
{
"service": "http_status:404"
}
],
"warp-routing": {
"enabled": true
}
}
`)
updateWithValidation(t, orchestrator, 2, configJSONV2)
configV2 := orchestrator.config
// Validate ingress rule 0
require.Equal(t, "jira.tunnel.org", configV2.Ingress.Rules[0].Hostname)
require.True(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/login"))
require.True(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/login/2fa"))
require.False(t, configV2.Ingress.Rules[0].Matches("jira.tunnel.org", "/users"))
require.Equal(t, "http://192.16.19.1:443", configV2.Ingress.Rules[0].Service.String())
require.Len(t, configV2.Ingress.Rules, 3)
// originRequest of this ingress rule overrides global default
require.Equal(t, time.Second*10, configV2.Ingress.Rules[0].Config.ConnectTimeout)
require.Equal(t, true, configV2.Ingress.Rules[0].Config.NoTLSVerify)
// Inherited from global default
require.Equal(t, true, configV2.Ingress.Rules[0].Config.NoHappyEyeballs)
// Validate ingress rule 1
require.Equal(t, "jira.tunnel.org", configV2.Ingress.Rules[1].Hostname)
require.True(t, configV2.Ingress.Rules[1].Matches("jira.tunnel.org", "/users"))
require.Equal(t, "http://172.32.20.6:80", configV2.Ingress.Rules[1].Service.String())
// originRequest of this ingress rule overrides global default
require.Equal(t, time.Second*30, configV2.Ingress.Rules[1].Config.ConnectTimeout)
require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoTLSVerify)
// Inherited from global default
require.Equal(t, true, configV2.Ingress.Rules[1].Config.NoHappyEyeballs)
// Validate ingress rule 2, it's the catch-all rule
require.True(t, configV2.Ingress.Rules[2].Matches("blogs.tunnel.io", "/2022/02/10"))
// Inherited from global default
require.Equal(t, time.Second*90, configV2.Ingress.Rules[2].Config.ConnectTimeout)
require.Equal(t, false, configV2.Ingress.Rules[2].Config.NoTLSVerify)
require.Equal(t, true, configV2.Ingress.Rules[2].Config.NoHappyEyeballs)
require.True(t, configV2.WarpRoutingEnabled)
originProxyV2, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
require.IsType(t, &proxy.Proxy{}, originProxyV2)
require.NotEqual(t, originProxyV2, initOriginProxy)
// Should not downgrade to an older version
resp := orchestrator.UpdateConfig(1, nil)
require.NoError(t, resp.Err)
require.Equal(t, int32(2), resp.LastAppliedVersion)
invalidJSON := []byte(`
{
"originRequest":
}
`)
resp = orchestrator.UpdateConfig(3, invalidJSON)
require.Error(t, resp.Err)
require.Equal(t, int32(2), resp.LastAppliedVersion)
originProxyV3, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
require.Equal(t, originProxyV2, originProxyV3)
configJSONV10 := []byte(`
{
"ingress": [
{
"service": "hello-world"
}
],
"warp-routing": {
"enabled": false
}
}
`)
updateWithValidation(t, orchestrator, 10, configJSONV10)
configV10 := orchestrator.config
require.Len(t, configV10.Ingress.Rules, 1)
require.True(t, configV10.Ingress.Rules[0].Matches("blogs.tunnel.io", "/2022/02/10"))
require.Equal(t, ingress.HelloWorldService, configV10.Ingress.Rules[0].Service.String())
require.False(t, configV10.WarpRoutingEnabled)
originProxyV10, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
require.IsType(t, &proxy.Proxy{}, originProxyV10)
require.NotEqual(t, originProxyV10, originProxyV2)
}
// TestConcurrentUpdateAndRead makes sure orchestrator can receive updates and return origin proxy concurrently
func TestConcurrentUpdateAndRead(t *testing.T) {
const (
concurrentRequests = 200
hostname = "public.tunnels.org"
expectedHost = "internal.tunnels.svc.cluster.local"
tcpBody = "testProxyTCP"
)
httpOrigin := httptest.NewServer(&validateHostHandler{
expectedHost: expectedHost,
body: t.Name(),
})
defer httpOrigin.Close()
tcpOrigin, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer tcpOrigin.Close()
var (
configJSONV1 = []byte(fmt.Sprintf(`
{
"originRequest": {
"connectTimeout": 90000000000,
"noHappyEyeballs": true
},
"ingress": [
{
"hostname": "%s",
"service": "%s",
"originRequest": {
"httpHostHeader": "%s",
"connectTimeout": 10000000000
}
},
{
"service": "http_status:404"
}
],
"warp-routing": {
"enabled": true
}
}
`, hostname, httpOrigin.URL, expectedHost))
configJSONV2 = []byte(`
{
"ingress": [
{
"service": "http_status:204"
}
],
"warp-routing": {
"enabled": false
}
}
`)
configJSONV3 = []byte(`
{
"ingress": [
{
"service": "http_status:418"
}
],
"warp-routing": {
"enabled": true
}
}
`)
// appliedV2 makes sure v3 is applied after v2
appliedV2 = make(chan struct{})
initConfig = &Config{
Ingress: &ingress.Ingress{},
WarpRoutingEnabled: false,
}
)
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, &testLogger)
require.NoError(t, err)
updateWithValidation(t, orchestrator, 1, configJSONV1)
var wg sync.WaitGroup
// tcpOrigin will be closed when the test exits. Only the handler routines are included in the wait group
go func() {
serveTCPOrigin(t, tcpOrigin, &wg)
}()
for i := 0; i < concurrentRequests; i++ {
originProxy, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
wg.Add(1)
go func(i int, originProxy connection.OriginProxy) {
defer wg.Done()
resp, err := proxyHTTP(t, originProxy, hostname)
require.NoError(t, err)
var warpRoutingDisabled bool
// The response can be from initOrigin, http_status:204 or http_status:418
switch resp.StatusCode {
// v1 proxy, warp enabled
case 200:
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, t.Name(), string(body))
warpRoutingDisabled = false
// v2 proxy, warp disabled
case 204:
require.Greater(t, i, concurrentRequests/4)
warpRoutingDisabled = true
// v3 proxy, warp enabled
case 418:
require.Greater(t, i, concurrentRequests/2)
warpRoutingDisabled = false
}
// Once we have originProxy, it won't be changed by configuration updates.
// We can infer the version by the ProxyHTTP response code
pr, pw := io.Pipe()
// concurrentRespWriter makes sure ResponseRecorder is not read/write concurrently, and read waits for the first write
w := newRespReadWriteFlusher()
// Write TCP message and make sure it's echo back. This has to be done in a go routune since ProxyTCP doesn't
// return until the stream is closed.
if !warpRoutingDisabled {
wg.Add(1)
go func() {
defer wg.Done()
defer pw.Close()
tcpEyeball(t, pw, tcpBody, w)
}()
}
proxyTCP(t, originProxy, tcpOrigin.Addr().String(), w, pr, warpRoutingDisabled)
}(i, originProxy)
if i == concurrentRequests/4 {
wg.Add(1)
go func() {
defer wg.Done()
updateWithValidation(t, orchestrator, 2, configJSONV2)
close(appliedV2)
}()
}
if i == concurrentRequests/2 {
wg.Add(1)
go func() {
defer wg.Done()
<-appliedV2
updateWithValidation(t, orchestrator, 3, configJSONV3)
}()
}
}
wg.Wait()
}
func proxyHTTP(t *testing.T, originProxy connection.OriginProxy, hostname string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", hostname), nil)
require.NoError(t, err)
w := httptest.NewRecorder()
respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeHTTP)
require.NoError(t, err)
err = originProxy.ProxyHTTP(respWriter, req, false)
if err != nil {
return nil, err
}
return w.Result(), nil
}
func tcpEyeball(t *testing.T, reqWriter io.WriteCloser, body string, respReadWriter *respReadWriteFlusher) {
writeN, err := reqWriter.Write([]byte(body))
require.NoError(t, err)
readBuffer := make([]byte, writeN)
n, err := respReadWriter.Read(readBuffer)
require.NoError(t, err)
require.Equal(t, body, string(readBuffer[:n]))
require.Equal(t, writeN, n)
}
func proxyTCP(t *testing.T, originProxy connection.OriginProxy, originAddr string, w http.ResponseWriter, reqBody io.ReadCloser, expectErr bool) {
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://%s", originAddr), reqBody)
require.NoError(t, err)
respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeTCP)
require.NoError(t, err)
tcpReq := &connection.TCPRequest{
Dest: originAddr,
CFRay: "123",
LBProbe: false,
}
rws := connection.NewHTTPResponseReadWriterAcker(respWriter, req)
if expectErr {
require.Error(t, originProxy.ProxyTCP(context.Background(), rws, tcpReq))
return
}
require.NoError(t, originProxy.ProxyTCP(context.Background(), rws, tcpReq))
}
func serveTCPOrigin(t *testing.T, tcpOrigin net.Listener, wg *sync.WaitGroup) {
for {
conn, err := tcpOrigin.Accept()
if err != nil {
return
}
wg.Add(1)
go func() {
defer wg.Done()
defer conn.Close()
echoTCP(t, conn)
}()
}
}
func echoTCP(t *testing.T, conn net.Conn) {
readBuf := make([]byte, 1000)
readN, err := conn.Read(readBuf)
require.NoError(t, err)
writeN, err := conn.Write(readBuf[:readN])
require.NoError(t, err)
require.Equal(t, readN, writeN)
}
type validateHostHandler struct {
expectedHost string
body string
}
func (vhh *validateHostHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Host != vhh.expectedHost {
w.WriteHeader(http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(vhh.body))
}
func updateWithValidation(t *testing.T, orchestrator *Orchestrator, version int32, config []byte) {
resp := orchestrator.UpdateConfig(version, config)
require.NoError(t, resp.Err)
require.Equal(t, version, resp.LastAppliedVersion)
}
// TestClosePreviousProxies makes sure proxies started in the pervious configuration version are shutdown
func TestClosePreviousProxies(t *testing.T) {
var (
hostname = "hello.tunnel1.org"
configWithHelloWorld = []byte(fmt.Sprintf(`
{
"ingress": [
{
"hostname": "%s",
"service": "hello-world"
},
{
"service": "http_status:404"
}
],
"warp-routing": {
"enabled": true
}
}
`, hostname))
configTeapot = []byte(`
{
"ingress": [
{
"service": "http_status:418"
}
],
"warp-routing": {
"enabled": true
}
}
`)
initConfig = &Config{
Ingress: &ingress.Ingress{},
WarpRoutingEnabled: false,
}
)
ctx, cancel := context.WithCancel(context.Background())
orchestrator, err := NewOrchestrator(ctx, initConfig, testTags, &testLogger)
require.NoError(t, err)
updateWithValidation(t, orchestrator, 1, configWithHelloWorld)
originProxyV1, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
resp, err := proxyHTTP(t, originProxyV1, hostname)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
updateWithValidation(t, orchestrator, 2, configTeapot)
originProxyV2, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
resp, err = proxyHTTP(t, originProxyV2, hostname)
require.NoError(t, err)
require.Equal(t, http.StatusTeapot, resp.StatusCode)
// The hello-world server in config v1 should have been stopped
resp, err = proxyHTTP(t, originProxyV1, hostname)
require.Error(t, err)
require.Nil(t, resp)
// Apply the config with hello world server again, orchestrator should spin up another hello world server
updateWithValidation(t, orchestrator, 3, configWithHelloWorld)
originProxyV3, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
require.NotEqual(t, originProxyV1, originProxyV3)
resp, err = proxyHTTP(t, originProxyV3, hostname)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
// cancel the context should terminate the last proxy
cancel()
// Wait for proxies to shutdown
time.Sleep(time.Millisecond * 10)
resp, err = proxyHTTP(t, originProxyV3, hostname)
require.Error(t, err)
require.Nil(t, resp)
}
// TestPersistentConnection makes sure updating the ingress doesn't intefere with existing connections
func TestPersistentConnection(t *testing.T) {
const (
hostname = "http://ws.tunnel.org"
)
msg := t.Name()
initConfig := &Config{
Ingress: &ingress.Ingress{},
WarpRoutingEnabled: false,
}
orchestrator, err := NewOrchestrator(context.Background(), initConfig, testTags, &testLogger)
require.NoError(t, err)
wsOrigin := httptest.NewServer(http.HandlerFunc(wsEcho))
defer wsOrigin.Close()
tcpOrigin, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer tcpOrigin.Close()
configWithWSAndWarp := []byte(fmt.Sprintf(`
{
"ingress": [
{
"service": "%s"
}
],
"warp-routing": {
"enabled": true
}
}
`, wsOrigin.URL))
updateWithValidation(t, orchestrator, 1, configWithWSAndWarp)
originProxy, err := orchestrator.GetOriginProxy()
require.NoError(t, err)
wsReqReader, wsReqWriter := io.Pipe()
wsRespReadWriter := newRespReadWriteFlusher()
tcpReqReader, tcpReqWriter := io.Pipe()
tcpRespReadWriter := newRespReadWriteFlusher()
var wg sync.WaitGroup
wg.Add(3)
// Start TCP origin
go func() {
defer wg.Done()
conn, err := tcpOrigin.Accept()
require.NoError(t, err)
defer conn.Close()
// Expect 3 TCP messages
for i := 0; i < 3; i++ {
echoTCP(t, conn)
}
}()
// Simulate cloudflared recieving a TCP connection
go func() {
defer wg.Done()
proxyTCP(t, originProxy, tcpOrigin.Addr().String(), tcpRespReadWriter, tcpReqReader, false)
}()
// Simulate cloudflared recieving a WS connection
go func() {
defer wg.Done()
req, err := http.NewRequest(http.MethodGet, hostname, wsReqReader)
require.NoError(t, err)
// ProxyHTTP will add Connection, Upgrade and Sec-Websocket-Version headers
req.Header.Add("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
respWriter, err := connection.NewHTTP2RespWriter(req, wsRespReadWriter, connection.TypeWebsocket)
require.NoError(t, err)
err = originProxy.ProxyHTTP(respWriter, req, true)
require.NoError(t, err)
}()
// Simulate eyeball WS and TCP connections
validateWsEcho(t, msg, wsReqWriter, wsRespReadWriter)
tcpEyeball(t, tcpReqWriter, msg, tcpRespReadWriter)
configNoWSAndWarp := []byte(`
{
"ingress": [
{
"service": "http_status:404"
}
],
"warp-routing": {
"enabled": false
}
}
`)
updateWithValidation(t, orchestrator, 2, configNoWSAndWarp)
// Make sure connection is still up
validateWsEcho(t, msg, wsReqWriter, wsRespReadWriter)
tcpEyeball(t, tcpReqWriter, msg, tcpRespReadWriter)
updateWithValidation(t, orchestrator, 3, configWithWSAndWarp)
// Make sure connection is still up
validateWsEcho(t, msg, wsReqWriter, wsRespReadWriter)
tcpEyeball(t, tcpReqWriter, msg, tcpRespReadWriter)
wsReqWriter.Close()
tcpReqWriter.Close()
wg.Wait()
}
func wsEcho(w http.ResponseWriter, r *http.Request) {
upgrader := gows.Upgrader{}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
defer conn.Close()
for {
mt, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("read message err", err)
break
}
err = conn.WriteMessage(mt, message)
if err != nil {
fmt.Println("write message err", err)
break
}
}
}
func validateWsEcho(t *testing.T, msg string, reqWriter io.Writer, respReadWriter io.ReadWriter) {
err := wsutil.WriteClientText(reqWriter, []byte(msg))
require.NoError(t, err)
receivedMsg, err := wsutil.ReadServerText(respReadWriter)
require.NoError(t, err)
require.Equal(t, msg, string(receivedMsg))
}
type respReadWriteFlusher struct {
io.Reader
w io.Writer
headers http.Header
statusCode int
setStatusOnce sync.Once
hasStatus chan struct{}
}
func newRespReadWriteFlusher() *respReadWriteFlusher {
pr, pw := io.Pipe()
return &respReadWriteFlusher{
Reader: pr,
w: pw,
headers: make(http.Header),
hasStatus: make(chan struct{}),
}
}
func (rrw *respReadWriteFlusher) Write(buf []byte) (int, error) {
rrw.WriteHeader(http.StatusOK)
return rrw.w.Write(buf)
}
func (rrw *respReadWriteFlusher) Flush() {}
func (rrw *respReadWriteFlusher) Header() http.Header {
return rrw.headers
}
func (rrw *respReadWriteFlusher) WriteHeader(statusCode int) {
rrw.setStatusOnce.Do(func() {
rrw.statusCode = statusCode
close(rrw.hasStatus)
})
}

View File

@ -1,4 +1,4 @@
package origin package proxy
import ( import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
@ -43,14 +43,6 @@ var (
Help: "Count of error proxying to origin", Help: "Count of error proxying to origin",
}, },
) )
haConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "ha_connections",
Help: "Number of active ha connections",
},
)
) )
func init() { func init() {
@ -59,7 +51,6 @@ func init() {
concurrentRequests, concurrentRequests,
responseByCode, responseByCode,
requestErrors, requestErrors,
haConnections,
) )
} }

View File

@ -1,4 +1,4 @@
package origin package proxy
import ( import (
"sync" "sync"

View File

@ -1,4 +1,4 @@
package origin package proxy
import ( import (
"bufio" "bufio"
@ -38,17 +38,22 @@ type Proxy struct {
// NewOriginProxy returns a new instance of the Proxy struct. // NewOriginProxy returns a new instance of the Proxy struct.
func NewOriginProxy( func NewOriginProxy(
ingressRules ingress.Ingress, ingressRules ingress.Ingress,
warpRouting *ingress.WarpRoutingService, warpRoutingEnabled bool,
tags []tunnelpogs.Tag, tags []tunnelpogs.Tag,
log *zerolog.Logger, log *zerolog.Logger,
) *Proxy { ) *Proxy {
return &Proxy{ proxy := &Proxy{
ingressRules: ingressRules, ingressRules: ingressRules,
warpRouting: warpRouting,
tags: tags, tags: tags,
log: log, log: log,
bufferPool: newBufferPool(512 * 1024), bufferPool: newBufferPool(512 * 1024),
} }
if warpRoutingEnabled {
proxy.warpRouting = ingress.NewWarpRoutingService()
log.Info().Msgf("Warp-routing is enabled")
}
return proxy
} }
// ProxyHTTP further depends on ingress rules to establish a connection with the origin service. This may be // ProxyHTTP further depends on ingress rules to establish a connection with the origin service. This may be

View File

@ -1,7 +1,7 @@
//go:build !windows //go:build !windows
// +build !windows // +build !windows
package origin package proxy
import ( import (
"io/ioutil" "io/ioutil"

View File

@ -1,4 +1,4 @@
package origin package proxy
import ( import (
"bytes" "bytes"
@ -31,8 +31,7 @@ import (
) )
var ( var (
testTags = []tunnelpogs.Tag{tunnelpogs.Tag{Name: "Name", Value: "value"}} testTags = []tunnelpogs.Tag{tunnelpogs.Tag{Name: "Name", Value: "value"}}
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
) )
type mockHTTPRespWriter struct { type mockHTTPRespWriter struct {
@ -131,17 +130,14 @@ func TestProxySingleOrigin(t *testing.T) {
ingressRule, err := ingress.NewSingleOrigin(cliCtx, allowURLFromArgs) ingressRule, err := ingress.NewSingleOrigin(cliCtx, allowURLFromArgs)
require.NoError(t, err) require.NoError(t, err)
var wg sync.WaitGroup require.NoError(t, ingressRule.StartOrigins(&log, ctx.Done()))
errC := make(chan error)
require.NoError(t, ingressRule.StartOrigins(&wg, &log, ctx.Done(), errC))
proxy := NewOriginProxy(ingressRule, unusedWarpRoutingService, testTags, &log) proxy := NewOriginProxy(ingressRule, false, testTags, &log)
t.Run("testProxyHTTP", testProxyHTTP(proxy)) t.Run("testProxyHTTP", testProxyHTTP(proxy))
t.Run("testProxyWebsocket", testProxyWebsocket(proxy)) t.Run("testProxyWebsocket", testProxyWebsocket(proxy))
t.Run("testProxySSE", testProxySSE(proxy)) t.Run("testProxySSE", testProxySSE(proxy))
t.Run("testProxySSEAllData", testProxySSEAllData(proxy)) t.Run("testProxySSEAllData", testProxySSEAllData(proxy))
cancel() cancel()
wg.Wait()
} }
func testProxyHTTP(proxy connection.OriginProxy) func(t *testing.T) { func testProxyHTTP(proxy connection.OriginProxy) func(t *testing.T) {
@ -341,11 +337,9 @@ func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.Unvalidat
log := zerolog.Nop() log := zerolog.Nop()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
errC := make(chan error) require.NoError(t, ingress.StartOrigins(&log, ctx.Done()))
var wg sync.WaitGroup
require.NoError(t, ingress.StartOrigins(&wg, &log, ctx.Done(), errC))
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log) proxy := NewOriginProxy(ingress, false, testTags, &log)
for _, test := range tests { for _, test := range tests {
responseWriter := newMockHTTPRespWriter() responseWriter := newMockHTTPRespWriter()
@ -363,7 +357,6 @@ func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.Unvalidat
} }
} }
cancel() cancel()
wg.Wait()
} }
type mockAPI struct{} type mockAPI struct{}
@ -394,7 +387,7 @@ func TestProxyError(t *testing.T) {
log := zerolog.Nop() log := zerolog.Nop()
proxy := NewOriginProxy(ing, unusedWarpRoutingService, testTags, &log) proxy := NewOriginProxy(ing, false, testTags, &log)
responseWriter := newMockHTTPRespWriter() responseWriter := newMockHTTPRespWriter()
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil) req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
@ -634,10 +627,9 @@ func TestConnections(t *testing.T) {
test.args.originService(t, ln) test.args.originService(t, ln)
ingressRule := createSingleIngressConfig(t, test.args.ingressServiceScheme+ln.Addr().String()) ingressRule := createSingleIngressConfig(t, test.args.ingressServiceScheme+ln.Addr().String())
var wg sync.WaitGroup ingressRule.StartOrigins(logger, ctx.Done())
errC := make(chan error) proxy := NewOriginProxy(ingressRule, true, testTags, logger)
ingressRule.StartOrigins(&wg, logger, ctx.Done(), errC) proxy.warpRouting = test.args.warpRoutingService
proxy := NewOriginProxy(ingressRule, test.args.warpRoutingService, testTags, logger)
dest := ln.Addr().String() dest := ln.Addr().String()
req, err := http.NewRequest( req, err := http.NewRequest(

View File

@ -17,8 +17,8 @@ import (
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
// The first 6 bytes of the stream is used to distinguish the type of stream. It ensures whoever performs a handshake does // ProtocolSignature defines the first 6 bytes of the stream, which is used to distinguish the type of stream. It
// not write data before writing the metadata. // ensures whoever performs a handshake does not write data before writing the metadata.
type ProtocolSignature [6]byte type ProtocolSignature [6]byte
var ( var (
@ -29,12 +29,15 @@ var (
RPCStreamProtocolSignature = ProtocolSignature{0x52, 0xBB, 0x82, 0x5C, 0xDB, 0x65} RPCStreamProtocolSignature = ProtocolSignature{0x52, 0xBB, 0x82, 0x5C, 0xDB, 0x65}
) )
const protocolVersionLength = 2
type protocolVersion string type protocolVersion string
const ( const (
protocolV1 protocolVersion = "01" protocolV1 protocolVersion = "01"
protocolVersionLength = 2
HandshakeIdleTimeout = 5 * time.Second
MaxIdleTimeout = 15 * time.Second
) )
// RequestServerStream is a stream to serve requests // RequestServerStream is a stream to serve requests
@ -122,7 +125,7 @@ func (rcs *RequestClientStream) ReadConnectResponseData() (*ConnectResponse, err
return nil, err return nil, err
} }
if signature != DataStreamProtocolSignature { if signature != DataStreamProtocolSignature {
return nil, fmt.Errorf("Wrong protocol signature %v", signature) return nil, fmt.Errorf("wrong protocol signature %v", signature)
} }
// This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions. // This is a NO-OP for now. We could cause a branching if we wanted to use multiple versions.
@ -154,13 +157,13 @@ func NewRPCServerStream(stream io.ReadWriteCloser, protocol ProtocolSignature) (
return &RPCServerStream{stream}, nil return &RPCServerStream{stream}, nil
} }
func (s *RPCServerStream) Serve(sessionManager tunnelpogs.SessionManager, logger *zerolog.Logger) error { func (s *RPCServerStream) Serve(sessionManager tunnelpogs.SessionManager, configManager tunnelpogs.ConfigurationManager, logger *zerolog.Logger) error {
// RPC logs are very robust, create a new logger that only logs error to reduce noise // RPC logs are very robust, create a new logger that only logs error to reduce noise
rpcLogger := logger.Level(zerolog.ErrorLevel) rpcLogger := logger.Level(zerolog.ErrorLevel)
rpcTransport := tunnelrpc.NewTransportLogger(&rpcLogger, rpc.StreamTransport(s)) rpcTransport := tunnelrpc.NewTransportLogger(&rpcLogger, rpc.StreamTransport(s))
defer rpcTransport.Close() defer rpcTransport.Close()
main := tunnelpogs.SessionManager_ServerToClient(sessionManager) main := tunnelpogs.CloudflaredServer_ServerToClient(sessionManager, configManager)
rpcConn := rpc.NewConn( rpcConn := rpc.NewConn(
rpcTransport, rpcTransport,
rpc.MainInterface(main.Client), rpc.MainInterface(main.Client),
@ -220,7 +223,7 @@ func writeSignature(stream io.Writer, signature ProtocolSignature) error {
// RPCClientStream is a stream to call methods of SessionManager // RPCClientStream is a stream to call methods of SessionManager
type RPCClientStream struct { type RPCClientStream struct {
client tunnelpogs.SessionManager_PogsClient client tunnelpogs.CloudflaredServer_PogsClient
transport rpc.Transport transport rpc.Transport
} }
@ -238,7 +241,7 @@ func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, logger *
tunnelrpc.ConnLog(logger), tunnelrpc.ConnLog(logger),
) )
return &RPCClientStream{ return &RPCClientStream{
client: tunnelpogs.SessionManager_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn}, client: tunnelpogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn),
transport: transport, transport: transport,
}, nil }, nil
} }
@ -255,6 +258,10 @@ func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID
return rcs.client.UnregisterUdpSession(ctx, sessionID, message) return rcs.client.UnregisterUdpSession(ctx, sessionID, message)
} }
func (rcs *RPCClientStream) UpdateConfiguration(ctx context.Context, version int32, config []byte) (*tunnelpogs.UpdateConfigurationResponse, error) {
return rcs.client.UpdateConfiguration(ctx, version, config)
}
func (rcs *RPCClientStream) Close() { func (rcs *RPCClientStream) Close() {
_ = rcs.client.Close() _ = rcs.client.Close()
_ = rcs.transport.Close() _ = rcs.transport.Close()

View File

@ -14,6 +14,8 @@ import (
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
) )
const ( const (
@ -108,14 +110,10 @@ func TestConnectResponseMeta(t *testing.T) {
} }
func TestRegisterUdpSession(t *testing.T) { func TestRegisterUdpSession(t *testing.T) {
clientReader, serverWriter := io.Pipe() clientStream, serverStream := newMockRPCStreams()
serverReader, clientWriter := io.Pipe()
clientStream := mockRPCStream{clientReader, clientWriter}
serverStream := mockRPCStream{serverReader, serverWriter}
unregisterMessage := "closed by eyeball" unregisterMessage := "closed by eyeball"
rpcServer := mockRPCServer{ sessionRPCServer := mockSessionRPCServer{
sessionID: uuid.New(), sessionID: uuid.New(),
dstIP: net.IP{172, 16, 0, 1}, dstIP: net.IP{172, 16, 0, 1},
dstPort: 8000, dstPort: 8000,
@ -129,7 +127,7 @@ func TestRegisterUdpSession(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
rpcServerStream, err := NewRPCServerStream(serverStream, protocol) rpcServerStream, err := NewRPCServerStream(serverStream, protocol)
assert.NoError(t, err) assert.NoError(t, err)
err = rpcServerStream.Serve(rpcServer, &logger) err = rpcServerStream.Serve(sessionRPCServer, nil, &logger)
assert.NoError(t, err) assert.NoError(t, err)
serverStream.Close() serverStream.Close()
@ -139,12 +137,12 @@ func TestRegisterUdpSession(t *testing.T) {
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger) rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger)
assert.NoError(t, err) assert.NoError(t, err)
assert.NoError(t, rpcClientStream.RegisterUdpSession(context.Background(), rpcServer.sessionID, rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint)) assert.NoError(t, rpcClientStream.RegisterUdpSession(context.Background(), sessionRPCServer.sessionID, sessionRPCServer.dstIP, sessionRPCServer.dstPort, testCloseIdleAfterHint))
// Different sessionID, the RPC server should reject the registraion // Different sessionID, the RPC server should reject the registraion
assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), rpcServer.dstIP, rpcServer.dstPort, testCloseIdleAfterHint)) assert.Error(t, rpcClientStream.RegisterUdpSession(context.Background(), uuid.New(), sessionRPCServer.dstIP, sessionRPCServer.dstPort, testCloseIdleAfterHint))
assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), rpcServer.sessionID, unregisterMessage)) assert.NoError(t, rpcClientStream.UnregisterUdpSession(context.Background(), sessionRPCServer.sessionID, unregisterMessage))
// Different sessionID, the RPC server should reject the unregistraion // Different sessionID, the RPC server should reject the unregistraion
assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New(), unregisterMessage)) assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New(), unregisterMessage))
@ -153,7 +151,48 @@ func TestRegisterUdpSession(t *testing.T) {
<-sessionRegisteredChan <-sessionRegisteredChan
} }
type mockRPCServer struct { func TestManageConfiguration(t *testing.T) {
var (
version int32 = 168
config = []byte(t.Name())
)
clientStream, serverStream := newMockRPCStreams()
configRPCServer := mockConfigRPCServer{
version: version,
config: config,
}
logger := zerolog.Nop()
updatedChan := make(chan struct{})
go func() {
protocol, err := DetermineProtocol(serverStream)
assert.NoError(t, err)
rpcServerStream, err := NewRPCServerStream(serverStream, protocol)
assert.NoError(t, err)
err = rpcServerStream.Serve(nil, configRPCServer, &logger)
assert.NoError(t, err)
serverStream.Close()
close(updatedChan)
}()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
rpcClientStream, err := NewRPCClientStream(ctx, clientStream, &logger)
assert.NoError(t, err)
result, err := rpcClientStream.UpdateConfiguration(ctx, version, config)
assert.NoError(t, err)
require.Equal(t, version, result.LastAppliedVersion)
require.NoError(t, result.Err)
rpcClientStream.Close()
<-updatedChan
}
type mockSessionRPCServer struct {
sessionID uuid.UUID sessionID uuid.UUID
dstIP net.IP dstIP net.IP
dstPort uint16 dstPort uint16
@ -161,7 +200,7 @@ type mockRPCServer struct {
unregisterMessage string unregisterMessage string
} }
func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error { func (s mockSessionRPCServer) RegisterUdpSession(_ context.Context, sessionID uuid.UUID, dstIP net.IP, dstPort uint16, closeIdleAfter time.Duration) error {
if s.sessionID != sessionID { if s.sessionID != sessionID {
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
} }
@ -177,7 +216,7 @@ func (s mockRPCServer) RegisterUdpSession(ctx context.Context, sessionID uuid.UU
return nil return nil
} }
func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.UUID, message string) error { func (s mockSessionRPCServer) UnregisterUdpSession(_ context.Context, sessionID uuid.UUID, message string) error {
if s.sessionID != sessionID { if s.sessionID != sessionID {
return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID) return fmt.Errorf("expect session ID %s, got %s", s.sessionID, sessionID)
} }
@ -187,11 +226,39 @@ func (s mockRPCServer) UnregisterUdpSession(ctx context.Context, sessionID uuid.
return nil return nil
} }
type mockConfigRPCServer struct {
version int32
config []byte
}
func (s mockConfigRPCServer) UpdateConfiguration(_ context.Context, version int32, config []byte) *tunnelpogs.UpdateConfigurationResponse {
if s.version != version {
return &tunnelpogs.UpdateConfigurationResponse{
Err: fmt.Errorf("expect version %d, got %d", s.version, version),
}
}
if !bytes.Equal(s.config, config) {
return &tunnelpogs.UpdateConfigurationResponse{
Err: fmt.Errorf("expect config %v, got %v", s.config, config),
}
}
return &tunnelpogs.UpdateConfigurationResponse{LastAppliedVersion: version}
}
type mockRPCStream struct { type mockRPCStream struct {
io.ReadCloser io.ReadCloser
io.WriteCloser io.WriteCloser
} }
func newMockRPCStreams() (client io.ReadWriteCloser, server io.ReadWriteCloser) {
clientReader, serverWriter := io.Pipe()
serverReader, clientWriter := io.Pipe()
client = mockRPCStream{clientReader, clientWriter}
server = mockRPCStream{serverReader, serverWriter}
return
}
func (s mockRPCStream) Close() error { func (s mockRPCStream) Close() error {
_ = s.ReadCloser.Close() _ = s.ReadCloser.Close()
_ = s.WriteCloser.Close() _ = s.WriteCloser.Close()

43
quic/safe_stream.go Normal file
View File

@ -0,0 +1,43 @@
package quic
import (
"sync"
"time"
"github.com/lucas-clemente/quic-go"
)
type SafeStreamCloser struct {
lock sync.Mutex
stream quic.Stream
}
func NewSafeStreamCloser(stream quic.Stream) *SafeStreamCloser {
return &SafeStreamCloser{
stream: stream,
}
}
func (s *SafeStreamCloser) Read(p []byte) (n int, err error) {
return s.stream.Read(p)
}
func (s *SafeStreamCloser) Write(p []byte) (n int, err error) {
s.lock.Lock()
defer s.lock.Unlock()
return s.stream.Write(p)
}
func (s *SafeStreamCloser) Close() error {
// Make sure a possible writer does not block the lock forever. We need it, so we can close the writer
// side of the stream safely.
_ = s.stream.SetWriteDeadline(time.Now())
// This lock is eventually acquired despite Write also acquiring it, because we set a deadline to writes.
s.lock.Lock()
defer s.lock.Unlock()
// We have to clean up the receiving stream ourselves since the Close in the bottom does not handle that.
s.stream.CancelRead(0)
return s.stream.Close()
}

142
quic/safe_stream_test.go Normal file
View File

@ -0,0 +1,142 @@
package quic
import (
"context"
"crypto/tls"
"io"
"net"
"sync"
"testing"
"github.com/lucas-clemente/quic-go"
"github.com/stretchr/testify/require"
)
var (
testTLSServerConfig = GenerateTLSConfig()
testQUICConfig = &quic.Config{
KeepAlive: true,
EnableDatagrams: true,
}
exchanges = 1000
msgsPerExchange = 10
testMsg = "Ok message"
)
func TestSafeStreamClose(t *testing.T) {
udpAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:0")
require.NoError(t, err)
udpListener, err := net.ListenUDP(udpAddr.Network(), udpAddr)
require.NoError(t, err)
defer udpListener.Close()
var serverReady sync.WaitGroup
serverReady.Add(1)
var done sync.WaitGroup
done.Add(1)
go func() {
defer done.Done()
quicServer(t, &serverReady, udpListener)
}()
done.Add(1)
go func() {
serverReady.Wait()
defer done.Done()
quicClient(t, udpListener.LocalAddr())
}()
done.Wait()
}
func quicClient(t *testing.T, addr net.Addr) {
tlsConf := &tls.Config{
InsecureSkipVerify: true,
NextProtos: []string{"argotunnel"},
}
session, err := quic.DialAddr(addr.String(), tlsConf, testQUICConfig)
require.NoError(t, err)
var wg sync.WaitGroup
for exchange := 0; exchange < exchanges; exchange++ {
quicStream, err := session.AcceptStream(context.Background())
require.NoError(t, err)
wg.Add(1)
go func(iter int) {
defer wg.Done()
stream := NewSafeStreamCloser(quicStream)
defer stream.Close()
// Do a bunch of round trips over this stream that should work.
for msg := 0; msg < msgsPerExchange; msg++ {
clientRoundTrip(t, stream, true)
}
// And one that won't work necessarily, but shouldn't break other streams in the session.
if iter%2 == 0 {
clientRoundTrip(t, stream, false)
}
}(exchange)
}
wg.Wait()
}
func quicServer(t *testing.T, serverReady *sync.WaitGroup, conn net.PacketConn) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
earlyListener, err := quic.Listen(conn, testTLSServerConfig, testQUICConfig)
require.NoError(t, err)
serverReady.Done()
session, err := earlyListener.Accept(ctx)
require.NoError(t, err)
var wg sync.WaitGroup
for exchange := 0; exchange < exchanges; exchange++ {
quicStream, err := session.OpenStreamSync(context.Background())
require.NoError(t, err)
wg.Add(1)
go func(iter int) {
defer wg.Done()
stream := NewSafeStreamCloser(quicStream)
defer stream.Close()
// Do a bunch of round trips over this stream that should work.
for msg := 0; msg < msgsPerExchange; msg++ {
serverRoundTrip(t, stream, true)
}
// And one that won't work necessarily, but shouldn't break other streams in the session.
if iter%2 == 1 {
serverRoundTrip(t, stream, false)
}
}(exchange)
}
wg.Wait()
}
func clientRoundTrip(t *testing.T, stream io.ReadWriteCloser, mustWork bool) {
response := make([]byte, len(testMsg))
_, err := stream.Read(response)
if !mustWork {
return
}
if err != io.EOF {
require.NoError(t, err)
}
require.Equal(t, testMsg, string(response))
}
func serverRoundTrip(t *testing.T, stream io.ReadWriteCloser, mustWork bool) {
_, err := stream.Write([]byte(testMsg))
if !mustWork {
return
}
require.NoError(t, err)
}

34
quic/test_utils.go Normal file
View File

@ -0,0 +1,34 @@
package quic
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"math/big"
)
// GenerateTLSConfig sets up a bare-bones TLS config for a QUIC server
func GenerateTLSConfig() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
panic(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
panic(err)
}
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
panic(err)
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: []string{"argotunnel"},
}
}

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"encoding/json" "encoding/json"

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"testing" "testing"

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"github.com/rs/zerolog" "github.com/rs/zerolog"

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"time" "time"

27
supervisor/metrics.go Normal file
View File

@ -0,0 +1,27 @@
package supervisor
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/cloudflare/cloudflared/connection"
)
// Metrics uses connection.MetricsNamespace(aka cloudflared) as namespace and connection.TunnelSubsystem
// (tunnel) as subsystem to keep them consistent with the previous qualifier.
var (
haConnections = prometheus.NewGauge(
prometheus.GaugeOpts{
Namespace: connection.MetricsNamespace,
Subsystem: connection.TunnelSubsystem,
Name: "ha_connections",
Help: "Number of active ha connections",
},
)
)
func init() {
prometheus.MustRegister(
haConnections,
)
}

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"context" "context"

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"context" "context"
@ -13,6 +13,7 @@ import (
"github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions" "github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/orchestration"
"github.com/cloudflare/cloudflared/retry" "github.com/cloudflare/cloudflared/retry"
"github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/signal"
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs" tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
@ -38,6 +39,7 @@ const (
type Supervisor struct { type Supervisor struct {
cloudflaredUUID uuid.UUID cloudflaredUUID uuid.UUID
config *TunnelConfig config *TunnelConfig
orchestrator *orchestration.Orchestrator
edgeIPs *edgediscovery.Edge edgeIPs *edgediscovery.Edge
tunnelErrors chan tunnelError tunnelErrors chan tunnelError
tunnelsConnecting map[int]chan struct{} tunnelsConnecting map[int]chan struct{}
@ -64,7 +66,7 @@ type tunnelError struct {
err error err error
} }
func NewSupervisor(config *TunnelConfig, reconnectCh chan ReconnectSignal, gracefulShutdownC <-chan struct{}) (*Supervisor, error) { func NewSupervisor(config *TunnelConfig, orchestrator *orchestration.Orchestrator, reconnectCh chan ReconnectSignal, gracefulShutdownC <-chan struct{}) (*Supervisor, error) {
cloudflaredUUID, err := uuid.NewRandom() cloudflaredUUID, err := uuid.NewRandom()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to generate cloudflared instance ID: %w", err) return nil, fmt.Errorf("failed to generate cloudflared instance ID: %w", err)
@ -88,6 +90,7 @@ func NewSupervisor(config *TunnelConfig, reconnectCh chan ReconnectSignal, grace
return &Supervisor{ return &Supervisor{
cloudflaredUUID: cloudflaredUUID, cloudflaredUUID: cloudflaredUUID,
config: config, config: config,
orchestrator: orchestrator,
edgeIPs: edgeIPs, edgeIPs: edgeIPs,
tunnelErrors: make(chan tunnelError), tunnelErrors: make(chan tunnelError),
tunnelsConnecting: map[int]chan struct{}{}, tunnelsConnecting: map[int]chan struct{}{},
@ -243,6 +246,7 @@ func (s *Supervisor) startFirstTunnel(
ctx, ctx,
s.reconnectCredentialManager, s.reconnectCredentialManager,
s.config, s.config,
s.orchestrator,
addr, addr,
s.log, s.log,
firstConnIndex, firstConnIndex,
@ -277,6 +281,7 @@ func (s *Supervisor) startFirstTunnel(
ctx, ctx,
s.reconnectCredentialManager, s.reconnectCredentialManager,
s.config, s.config,
s.orchestrator,
addr, addr,
s.log, s.log,
firstConnIndex, firstConnIndex,
@ -311,6 +316,7 @@ func (s *Supervisor) startTunnel(
ctx, ctx,
s.reconnectCredentialManager, s.reconnectCredentialManager,
s.config, s.config,
s.orchestrator,
addr, addr,
s.log, s.log,
uint8(index), uint8(index),
@ -380,7 +386,7 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
defer rpcClient.Close() defer rpcClient.Close()
const arbitraryConnectionID = uint8(0) const arbitraryConnectionID = uint8(0)
registrationOptions := s.config.RegistrationOptions(arbitraryConnectionID, edgeConn.LocalAddr().String(), s.cloudflaredUUID) registrationOptions := s.config.registrationOptions(arbitraryConnectionID, edgeConn.LocalAddr().String(), s.cloudflaredUUID)
registrationOptions.NumPreviousAttempts = uint8(numPreviousAttempts) registrationOptions.NumPreviousAttempts = uint8(numPreviousAttempts)
return rpcClient.Authenticate(ctx, s.config.ClassicTunnel, registrationOptions) return rpcClient.Authenticate(ctx, s.config.ClassicTunnel, registrationOptions)
} }

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"context" "context"
@ -20,6 +20,7 @@ import (
"github.com/cloudflare/cloudflared/edgediscovery" "github.com/cloudflare/cloudflared/edgediscovery"
"github.com/cloudflare/cloudflared/edgediscovery/allregions" "github.com/cloudflare/cloudflared/edgediscovery/allregions"
"github.com/cloudflare/cloudflared/h2mux" "github.com/cloudflare/cloudflared/h2mux"
"github.com/cloudflare/cloudflared/orchestration"
quicpogs "github.com/cloudflare/cloudflared/quic" quicpogs "github.com/cloudflare/cloudflared/quic"
"github.com/cloudflare/cloudflared/retry" "github.com/cloudflare/cloudflared/retry"
"github.com/cloudflare/cloudflared/signal" "github.com/cloudflare/cloudflared/signal"
@ -31,37 +32,36 @@ const (
dialTimeout = 15 * time.Second dialTimeout = 15 * time.Second
FeatureSerializedHeaders = "serialized_headers" FeatureSerializedHeaders = "serialized_headers"
FeatureQuickReconnects = "quick_reconnects" FeatureQuickReconnects = "quick_reconnects"
quicHandshakeIdleTimeout = 5 * time.Second
quicMaxIdleTimeout = 15 * time.Second
) )
type TunnelConfig struct { type TunnelConfig struct {
ConnectionConfig *connection.Config GracePeriod time.Duration
OSArch string ReplaceExisting bool
ClientID string OSArch string
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once ClientID string
EdgeAddrs []string CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
Region string EdgeAddrs []string
HAConnections int Region string
IncidentLookup IncidentLookup HAConnections int
IsAutoupdated bool IncidentLookup IncidentLookup
LBPool string IsAutoupdated bool
Tags []tunnelpogs.Tag LBPool string
Log *zerolog.Logger Tags []tunnelpogs.Tag
LogTransport *zerolog.Logger Log *zerolog.Logger
Observer *connection.Observer LogTransport *zerolog.Logger
ReportedVersion string Observer *connection.Observer
Retries uint ReportedVersion string
RunFromTerminal bool Retries uint
RunFromTerminal bool
NamedTunnel *connection.NamedTunnelConfig NamedTunnel *connection.NamedTunnelProperties
ClassicTunnel *connection.ClassicTunnelConfig ClassicTunnel *connection.ClassicTunnelProperties
MuxerConfig *connection.MuxerConfig MuxerConfig *connection.MuxerConfig
ProtocolSelector connection.ProtocolSelector ProtocolSelector connection.ProtocolSelector
EdgeTLSConfigs map[connection.Protocol]*tls.Config EdgeTLSConfigs map[connection.Protocol]*tls.Config
} }
func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP string, uuid uuid.UUID) *tunnelpogs.RegistrationOptions { func (c *TunnelConfig) registrationOptions(connectionID uint8, OriginLocalIP string, uuid uuid.UUID) *tunnelpogs.RegistrationOptions {
policy := tunnelrpc.ExistingTunnelPolicy_balance policy := tunnelrpc.ExistingTunnelPolicy_balance
if c.HAConnections <= 1 && c.LBPool == "" { if c.HAConnections <= 1 && c.LBPool == "" {
policy = tunnelrpc.ExistingTunnelPolicy_disconnect policy = tunnelrpc.ExistingTunnelPolicy_disconnect
@ -83,7 +83,7 @@ func (c *TunnelConfig) RegistrationOptions(connectionID uint8, OriginLocalIP str
} }
} }
func (c *TunnelConfig) ConnectionOptions(originLocalAddr string, numPreviousAttempts uint8) *tunnelpogs.ConnectionOptions { func (c *TunnelConfig) connectionOptions(originLocalAddr string, numPreviousAttempts uint8) *tunnelpogs.ConnectionOptions {
// attempt to parse out origin IP, but don't fail since it's informational field // attempt to parse out origin IP, but don't fail since it's informational field
host, _, _ := net.SplitHostPort(originLocalAddr) host, _, _ := net.SplitHostPort(originLocalAddr)
originIP := net.ParseIP(host) originIP := net.ParseIP(host)
@ -91,7 +91,7 @@ func (c *TunnelConfig) ConnectionOptions(originLocalAddr string, numPreviousAtte
return &tunnelpogs.ConnectionOptions{ return &tunnelpogs.ConnectionOptions{
Client: c.NamedTunnel.Client, Client: c.NamedTunnel.Client,
OriginLocalIP: originIP, OriginLocalIP: originIP,
ReplaceExisting: c.ConnectionConfig.ReplaceExisting, ReplaceExisting: c.ReplaceExisting,
CompressionQuality: uint8(c.MuxerConfig.CompressionSetting), CompressionQuality: uint8(c.MuxerConfig.CompressionSetting),
NumPreviousAttempts: numPreviousAttempts, NumPreviousAttempts: numPreviousAttempts,
} }
@ -108,11 +108,12 @@ func (c *TunnelConfig) SupportedFeatures() []string {
func StartTunnelDaemon( func StartTunnelDaemon(
ctx context.Context, ctx context.Context,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
connectedSignal *signal.Signal, connectedSignal *signal.Signal,
reconnectCh chan ReconnectSignal, reconnectCh chan ReconnectSignal,
graceShutdownC <-chan struct{}, graceShutdownC <-chan struct{},
) error { ) error {
s, err := NewSupervisor(config, reconnectCh, graceShutdownC) s, err := NewSupervisor(config, orchestrator, reconnectCh, graceShutdownC)
if err != nil { if err != nil {
return err return err
} }
@ -123,6 +124,7 @@ func ServeTunnelLoop(
ctx context.Context, ctx context.Context,
credentialManager *reconnectCredentialManager, credentialManager *reconnectCredentialManager,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
addr *allregions.EdgeAddr, addr *allregions.EdgeAddr,
connAwareLogger *ConnAwareLogger, connAwareLogger *ConnAwareLogger,
connIndex uint8, connIndex uint8,
@ -158,6 +160,7 @@ func ServeTunnelLoop(
connLog, connLog,
credentialManager, credentialManager,
config, config,
orchestrator,
addr, addr,
connIndex, connIndex,
connectedFuse, connectedFuse,
@ -256,6 +259,7 @@ func ServeTunnel(
connLog *ConnAwareLogger, connLog *ConnAwareLogger,
credentialManager *reconnectCredentialManager, credentialManager *reconnectCredentialManager,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
addr *allregions.EdgeAddr, addr *allregions.EdgeAddr,
connIndex uint8, connIndex uint8,
fuse *h2mux.BooleanFuse, fuse *h2mux.BooleanFuse,
@ -284,6 +288,7 @@ func ServeTunnel(
connLog, connLog,
credentialManager, credentialManager,
config, config,
orchestrator,
addr, addr,
connIndex, connIndex,
fuse, fuse,
@ -332,6 +337,7 @@ func serveTunnel(
connLog *ConnAwareLogger, connLog *ConnAwareLogger,
credentialManager *reconnectCredentialManager, credentialManager *reconnectCredentialManager,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
addr *allregions.EdgeAddr, addr *allregions.EdgeAddr,
connIndex uint8, connIndex uint8,
fuse *h2mux.BooleanFuse, fuse *h2mux.BooleanFuse,
@ -341,7 +347,6 @@ func serveTunnel(
protocol connection.Protocol, protocol connection.Protocol,
gracefulShutdownC <-chan struct{}, gracefulShutdownC <-chan struct{},
) (err error, recoverable bool) { ) (err error, recoverable bool) {
connectedFuse := &connectedFuse{ connectedFuse := &connectedFuse{
fuse: fuse, fuse: fuse,
backoff: backoff, backoff: backoff,
@ -353,15 +358,16 @@ func serveTunnel(
connIndex, connIndex,
nil, nil,
gracefulShutdownC, gracefulShutdownC,
config.ConnectionConfig.GracePeriod, config.GracePeriod,
) )
switch protocol { switch protocol {
case connection.QUIC, connection.QUICWarp: case connection.QUIC, connection.QUICWarp:
connOptions := config.ConnectionOptions(addr.UDP.String(), uint8(backoff.Retries())) connOptions := config.connectionOptions(addr.UDP.String(), uint8(backoff.Retries()))
return ServeQUIC(ctx, return ServeQUIC(ctx,
addr.UDP, addr.UDP,
config, config,
orchestrator,
connLog, connLog,
connOptions, connOptions,
controlStream, controlStream,
@ -376,11 +382,12 @@ func serveTunnel(
return err, true return err, true
} }
connOptions := config.ConnectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries())) connOptions := config.connectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries()))
if err := ServeHTTP2( if err := ServeHTTP2(
ctx, ctx,
connLog, connLog,
config, config,
orchestrator,
edgeConn, edgeConn,
connOptions, connOptions,
controlStream, controlStream,
@ -403,6 +410,7 @@ func serveTunnel(
connLog, connLog,
credentialManager, credentialManager,
config, config,
orchestrator,
edgeConn, edgeConn,
connIndex, connIndex,
connectedFuse, connectedFuse,
@ -429,6 +437,7 @@ func ServeH2mux(
connLog *ConnAwareLogger, connLog *ConnAwareLogger,
credentialManager *reconnectCredentialManager, credentialManager *reconnectCredentialManager,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
edgeConn net.Conn, edgeConn net.Conn,
connIndex uint8, connIndex uint8,
connectedFuse *connectedFuse, connectedFuse *connectedFuse,
@ -439,7 +448,8 @@ func ServeH2mux(
connLog.Logger().Debug().Msgf("Connecting via h2mux") connLog.Logger().Debug().Msgf("Connecting via h2mux")
// Returns error from parsing the origin URL or handshake errors // Returns error from parsing the origin URL or handshake errors
handler, err, recoverable := connection.NewH2muxConnection( handler, err, recoverable := connection.NewH2muxConnection(
config.ConnectionConfig, orchestrator,
config.GracePeriod,
config.MuxerConfig, config.MuxerConfig,
edgeConn, edgeConn,
connIndex, connIndex,
@ -457,10 +467,10 @@ func ServeH2mux(
errGroup.Go(func() error { errGroup.Go(func() error {
if config.NamedTunnel != nil { if config.NamedTunnel != nil {
connOptions := config.ConnectionOptions(edgeConn.LocalAddr().String(), uint8(connectedFuse.backoff.Retries())) connOptions := config.connectionOptions(edgeConn.LocalAddr().String(), uint8(connectedFuse.backoff.Retries()))
return handler.ServeNamedTunnel(serveCtx, config.NamedTunnel, connOptions, connectedFuse) return handler.ServeNamedTunnel(serveCtx, config.NamedTunnel, connOptions, connectedFuse)
} }
registrationOptions := config.RegistrationOptions(connIndex, edgeConn.LocalAddr().String(), cloudflaredUUID) registrationOptions := config.registrationOptions(connIndex, edgeConn.LocalAddr().String(), cloudflaredUUID)
return handler.ServeClassicTunnel(serveCtx, config.ClassicTunnel, credentialManager, registrationOptions, connectedFuse) return handler.ServeClassicTunnel(serveCtx, config.ClassicTunnel, credentialManager, registrationOptions, connectedFuse)
}) })
@ -475,6 +485,7 @@ func ServeHTTP2(
ctx context.Context, ctx context.Context,
connLog *ConnAwareLogger, connLog *ConnAwareLogger,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
tlsServerConn net.Conn, tlsServerConn net.Conn,
connOptions *tunnelpogs.ConnectionOptions, connOptions *tunnelpogs.ConnectionOptions,
controlStreamHandler connection.ControlStreamHandler, controlStreamHandler connection.ControlStreamHandler,
@ -485,7 +496,7 @@ func ServeHTTP2(
connLog.Logger().Debug().Msgf("Connecting via http2") connLog.Logger().Debug().Msgf("Connecting via http2")
h2conn := connection.NewHTTP2Connection( h2conn := connection.NewHTTP2Connection(
tlsServerConn, tlsServerConn,
config.ConnectionConfig, orchestrator,
connOptions, connOptions,
config.Observer, config.Observer,
connIndex, connIndex,
@ -514,6 +525,7 @@ func ServeQUIC(
ctx context.Context, ctx context.Context,
edgeAddr *net.UDPAddr, edgeAddr *net.UDPAddr,
config *TunnelConfig, config *TunnelConfig,
orchestrator *orchestration.Orchestrator,
connLogger *ConnAwareLogger, connLogger *ConnAwareLogger,
connOptions *tunnelpogs.ConnectionOptions, connOptions *tunnelpogs.ConnectionOptions,
controlStreamHandler connection.ControlStreamHandler, controlStreamHandler connection.ControlStreamHandler,
@ -523,8 +535,8 @@ func ServeQUIC(
) (err error, recoverable bool) { ) (err error, recoverable bool) {
tlsConfig := config.EdgeTLSConfigs[connection.QUIC] tlsConfig := config.EdgeTLSConfigs[connection.QUIC]
quicConfig := &quic.Config{ quicConfig := &quic.Config{
HandshakeIdleTimeout: quicHandshakeIdleTimeout, HandshakeIdleTimeout: quicpogs.HandshakeIdleTimeout,
MaxIdleTimeout: quicMaxIdleTimeout, MaxIdleTimeout: quicpogs.MaxIdleTimeout,
MaxIncomingStreams: connection.MaxConcurrentStreams, MaxIncomingStreams: connection.MaxConcurrentStreams,
MaxIncomingUniStreams: connection.MaxConcurrentStreams, MaxIncomingUniStreams: connection.MaxConcurrentStreams,
KeepAlive: true, KeepAlive: true,
@ -537,7 +549,7 @@ func ServeQUIC(
quicConfig, quicConfig,
edgeAddr, edgeAddr,
tlsConfig, tlsConfig,
config.ConnectionConfig.OriginProxy, orchestrator,
connOptions, connOptions,
controlStreamHandler, controlStreamHandler,
connLogger.Logger()) connLogger.Logger())

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"testing" "testing"
@ -32,11 +32,7 @@ func TestWaitForBackoffFallback(t *testing.T) {
} }
log := zerolog.Nop() log := zerolog.Nop()
resolveTTL := time.Duration(0) resolveTTL := time.Duration(0)
namedTunnel := &connection.NamedTunnelConfig{ namedTunnel := &connection.NamedTunnelProperties{}
Credentials: connection.Credentials{
AccountTag: "test-account",
},
}
mockFetcher := dynamicMockFetcher{ mockFetcher := dynamicMockFetcher{
protocolPercents: edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}}, protocolPercents: edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}},
} }

View File

@ -1,4 +1,4 @@
package origin package supervisor
import ( import (
"fmt" "fmt"

View File

@ -0,0 +1,53 @@
package pogs
import (
"github.com/cloudflare/cloudflared/tunnelrpc"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/rpc"
)
type CloudflaredServer interface {
SessionManager
ConfigurationManager
}
type CloudflaredServer_PogsImpl struct {
SessionManager_PogsImpl
ConfigurationManager_PogsImpl
}
func CloudflaredServer_ServerToClient(s SessionManager, c ConfigurationManager) tunnelrpc.CloudflaredServer {
return tunnelrpc.CloudflaredServer_ServerToClient(CloudflaredServer_PogsImpl{
SessionManager_PogsImpl: SessionManager_PogsImpl{s},
ConfigurationManager_PogsImpl: ConfigurationManager_PogsImpl{c},
})
}
type CloudflaredServer_PogsClient struct {
SessionManager_PogsClient
ConfigurationManager_PogsClient
Client capnp.Client
Conn *rpc.Conn
}
func NewCloudflaredServer_PogsClient(client capnp.Client, conn *rpc.Conn) CloudflaredServer_PogsClient {
sessionManagerClient := SessionManager_PogsClient{
Client: client,
Conn: conn,
}
configManagerClient := ConfigurationManager_PogsClient{
Client: client,
Conn: conn,
}
return CloudflaredServer_PogsClient{
SessionManager_PogsClient: sessionManagerClient,
ConfigurationManager_PogsClient: configManagerClient,
Client: client,
Conn: conn,
}
}
func (c CloudflaredServer_PogsClient) Close() error {
c.Client.Close()
return c.Conn.Close()
}

View File

@ -0,0 +1,95 @@
package pogs
import (
"context"
"fmt"
"github.com/cloudflare/cloudflared/tunnelrpc"
capnp "zombiezen.com/go/capnproto2"
"zombiezen.com/go/capnproto2/rpc"
"zombiezen.com/go/capnproto2/server"
)
type ConfigurationManager interface {
UpdateConfiguration(ctx context.Context, version int32, config []byte) *UpdateConfigurationResponse
}
type ConfigurationManager_PogsImpl struct {
impl ConfigurationManager
}
func ConfigurationManager_ServerToClient(c ConfigurationManager) tunnelrpc.ConfigurationManager {
return tunnelrpc.ConfigurationManager_ServerToClient(ConfigurationManager_PogsImpl{c})
}
func (i ConfigurationManager_PogsImpl) UpdateConfiguration(p tunnelrpc.ConfigurationManager_updateConfiguration) error {
server.Ack(p.Options)
version := p.Params.Version()
config, err := p.Params.Config()
if err != nil {
return err
}
result, err := p.Results.NewResult()
if err != nil {
return err
}
updateResp := i.impl.UpdateConfiguration(p.Ctx, version, config)
return updateResp.Marshal(result)
}
type ConfigurationManager_PogsClient struct {
Client capnp.Client
Conn *rpc.Conn
}
func (c ConfigurationManager_PogsClient) Close() error {
c.Client.Close()
return c.Conn.Close()
}
func (c ConfigurationManager_PogsClient) UpdateConfiguration(ctx context.Context, version int32, config []byte) (*UpdateConfigurationResponse, error) {
client := tunnelrpc.ConfigurationManager{Client: c.Client}
promise := client.UpdateConfiguration(ctx, func(p tunnelrpc.ConfigurationManager_updateConfiguration_Params) error {
p.SetVersion(version)
return p.SetConfig(config)
})
result, err := promise.Result().Struct()
if err != nil {
return nil, wrapRPCError(err)
}
response := new(UpdateConfigurationResponse)
err = response.Unmarshal(result)
if err != nil {
return nil, err
}
return response, nil
}
type UpdateConfigurationResponse struct {
LastAppliedVersion int32 `json:"lastAppliedVersion"`
Err error `json:"err"`
}
func (p *UpdateConfigurationResponse) Marshal(s tunnelrpc.UpdateConfigurationResponse) error {
s.SetLatestAppliedVersion(p.LastAppliedVersion)
if p.Err != nil {
return s.SetErr(p.Err.Error())
}
return nil
}
func (p *UpdateConfigurationResponse) Unmarshal(s tunnelrpc.UpdateConfigurationResponse) error {
p.LastAppliedVersion = s.LatestAppliedVersion()
respErr, err := s.Err()
if err != nil {
return err
}
if respErr != "" {
p.Err = fmt.Errorf(respErr)
}
return nil
}

View File

@ -151,4 +151,19 @@ interface SessionManager {
# Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side # Let the edge decide closeAfterIdle to make sure cloudflared doesn't close session before the edge closes its side
registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16, closeAfterIdleHint: Int64) -> (result :RegisterUdpSessionResponse); registerUdpSession @0 (sessionId :Data, dstIp :Data, dstPort: UInt16, closeAfterIdleHint: Int64) -> (result :RegisterUdpSessionResponse);
unregisterUdpSession @1 (sessionId :Data, message: Text) -> (); unregisterUdpSession @1 (sessionId :Data, message: Text) -> ();
} }
struct UpdateConfigurationResponse {
# Latest configuration that was applied successfully. The err field might be populated at the same time to indicate
# that cloudflared is using an older configuration because the latest cannot be applied
latestAppliedVersion @0 :Int32;
# Any error encountered when trying to apply the last configuration
err @1 :Text;
}
# ConfigurationManager defines RPC to manage cloudflared configuration remotely
interface ConfigurationManager {
updateConfiguration @0 (version :Int32, config :Data) -> (result: UpdateConfigurationResponse);
}
interface CloudflaredServer extends(SessionManager, ConfigurationManager) {}

View File

@ -3880,204 +3880,661 @@ func (p SessionManager_unregisterUdpSession_Results_Promise) Struct() (SessionMa
return SessionManager_unregisterUdpSession_Results{s}, err return SessionManager_unregisterUdpSession_Results{s}, err
} }
const schema_db8274f9144abc7e = "x\xda\xccY}p\x14e\x9a\x7f\x9e\xee\x99t\x02\x19" + type UpdateConfigurationResponse struct{ capnp.Struct }
"f\xbaz 0%\x97\x93\xc2\xf2\x88\x82\x06\xce+\x8e" +
"\xb3.\x09\x06\xceD>\xd23p\xe5\x09Zvf\xde" + // UpdateConfigurationResponse_TypeID is the unique identifier for the type UpdateConfigurationResponse.
"\x84\xc9\xcdt\x0f\xdd=\x91 \xc8\x87 b\xf9\x05\x82" + const UpdateConfigurationResponse_TypeID = 0xdb58ff694ba05cf9
"\"\xca\xc9ayW\xa0\xde\xc1\xa9\xe7\xb2%\xb5\xb2+" +
"*\xa5\xa8X\xb0\x85\x8a\xb5\x8b\xc8\xeeJ\xc1\xba\"\xac" + func NewUpdateConfigurationResponse(s *capnp.Segment) (UpdateConfigurationResponse, error) {
"\xe5\xaeko=\xdd\xd3\x1f\x99\x84$\xc8\xfe\xb1\xffM" + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1})
"\x9e~\xde\xf7}>~\xcf\xef}\xde'\xd7wT6" + return UpdateConfigurationResponse{st}, err
"r\xf5\xe1\x9a\x08\x80\xbc%\\a\xb1\xba\x0f\x97n\xbf" + }
"\xeag\xabAN Z\xf7\xbc\xd6\x1a\xff\xd6\\\xfd\x09" +
"\x84y\x01`\xca\xe2\x8a\xa5(\xad\xad\x10\x00\xa4U\x15" + func NewRootUpdateConfigurationResponse(s *capnp.Segment) (UpdateConfigurationResponse, error) {
"\xbf\x06\xb4\xee\x1b\xb5\xfb\x99\xe7fl\xba\x17\xc4\x04\xef" + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1})
"+\x03NaB+J=\x02i\x16\x85u\xd2Q\xfa" + return UpdateConfigurationResponse{st}, err
"e\xdd\"^\xb7 \xfe\xc1{\xa4\x1d\xdc:D[\xef" + }
"\x13\xeaP:d/8(\xd0\xd67\xe6\xdf\xdf\xf1\x0f" +
"\x9b\xdfY\x03b\x82\xeb\xb5\xf5+\x95KQ:XI" + func ReadRootUpdateConfigurationResponse(msg *capnp.Message) (UpdateConfigurationResponse, error) {
"\x9a\x07*\xe7\x02Z_o\x1a\xfd\xfc\x7f\xbe\xf7\xf6Z" + root, err := msg.RootPtr()
"\x10\xafF(Y\xfai\xe5\xc7\x08(}U\xf9\xbf\x80" + return UpdateConfigurationResponse{root.Struct()}, err
"\xd6\xa1\x0b\x0b\xce\xbf\xfc\xe6\x0d\xf7\x818\x81\x14\x90\x14" + }
"6T\x8d\xe3\x00\xa5\x9dU\x0d\x80\xd6\xe93\x7f\\w" +
"\xf7\x849\x8f\x82<\x019\x800G\x1a\x07\xab\x12\xa4" + func (s UpdateConfigurationResponse) String() string {
"q\xa2\x8a\xaci\x98yhob\xca\xe3\x9b\xcaL\xb7" + str, _ := text.Marshal(0xdb58ff694ba05cf9, s.Struct)
"\x15\xf7\x0f\xabC\xe9\xf002\xe8\xd0\xb0\xbb\x00\xad\xdf" + return str
"\x8fx\xea\xbd\xe2M\xaf>^:\xcfV\xaa\x1f^G" + }
"\xbb\xb5\x0c'\x85q\xddW\xdd\xf9\xd3\x03/=\x01\xf2" +
"DD\xebx\xfb5G\xf9m\xbb>\x81\xf9(\xd0\xf1" + func (s UpdateConfigurationResponse) LatestAppliedVersion() int32 {
"Sv\x0e\xdfA\xc6\xef\xb5u\xdf\xbf\xf6\xb5\x1f?\xfa" + return int32(s.Struct.Uint32(0))
"\xd2\xba\xa7@\xbe\x1a\x11\xc0\x0e\xd6\xd8\xea?\x90B}" + }
"5\x19\xbf\xe9\xd8\xbe9\xf9\x0d[w8\xee\xdb\xdf\xff" +
"\xad\x9a\xe3 d\xadi\xf9&?\xff\xd9\xd4\xb3\xa5\xc0" + func (s UpdateConfigurationResponse) SetLatestAppliedVersion(v int32) {
"\x84\xe9\xd3\xec\xeas\x088E\xa9\xaeE@\xeb\x86\x8f" + s.Struct.SetUint32(0, uint32(v))
"O\xcd\x9d\xfd\x7f\x1d\xff\x1dX\xbb<\xb2\x94\xd6\xae\xeb" + }
"8\xb7?\x96\xcc?_\xe6\xb0\x1d\xbb\x9e\xc8.\x946" +
"D\xc8\xe1\x87\"d\xc2\x8b\x7fsK\xd5\x92S3w" + func (s UpdateConfigurationResponse) Err() (string, error) {
"\x838\xd1\xdd\xe6\xc5H\x92\xb6\x09\xdd\xce\x7f\xafl\xf9" + p, err := s.Struct.Ptr(0)
"\xc9\xcb\xe5p\xb2c\xb23\xd2\x8e\xd2>\xdag\xca\xde" + return p.Text(), err
"\x88m\xcf\x03\xfb\xb7^S\xf9\xcc\xd7\xaf\xf4\x17\xe6\x13" + }
"#\xdaQ\xba0\x82N\xfdj\x04Efd\x0b\x1e\x7f" +
"\xbd>\xf4j0\xefr\xf44E\x86E)\xefc\xcf" + func (s UpdateConfigurationResponse) HasErr() bool {
"N\x8f\xa8_\xae~\xbdl7[1\x1ckEiL" + p, err := s.Struct.Ptr(0)
"\x8cv\x1b\x19#\xe5\xd6\x05\x8fm\x0c\x9fz\xec-\xb2" + return p.IsValid() || err != nil
"4\x00\xb80\x01m\xca\x9e\x98\x8e\xd2\x81\x98\x9d\xedX" + }
"\x0d\x0fh%v\xff\xd3\xffL\xcf|\xf4N?\x96J" +
"M\xf1s\xd2\xec8\xfdj\x89\x93\xa1''\xee\xb9\xfb" + func (s UpdateConfigurationResponse) ErrBytes() ([]byte, error) {
"\x8b\x87\x0e\x1f)\x19j\xc7\xf0\xb9\xb8\x9d\xc2\xbdq\x8a" + p, err := s.Struct.Ptr(0)
"\x9f\x87\x80\xb2(\xd9\x9a\x1f\xc5\xbbP:ko\xf7\x85" + return p.TextBytes(), err
"\xad\xcd\x9dR\xc6\xac\xfc\xf9?\x1f\x0f$\xedl\xfc3" + }
"\x84\x905\xe7_\x17tU-?y2x\xd0\x89\xb8" +
"\x1d\x91\x0b\xf6\xd2\xdf\xfe\xd7\xe9G\xce\xe43\xbf\xb2\x81" + func (s UpdateConfigurationResponse) SetErr(v string) error {
"\xe7\xc6l\xe4\xc8i\x04\xcd\x89#\x09\xe85\xb5\x91\x19" + return s.Struct.SetText(0, v)
"\xe3\x8e\xb5\x9dvR\xe9lQ5j:)\\9\x8a" + }
"\xb6\xb8\xe1\xce&\xb6p\xea\xad\xa7\xfb\x94|\xd3\xa8i" +
"(\xc9\xa3l\x90\x8dZ\x87\x12\xab\xa9\x01\xb0\xba\xff\x7f" + // UpdateConfigurationResponse_List is a list of UpdateConfigurationResponse.
"\xc3\xad\xcf\xbf1\xe7\x9cS\x0b\xb6\xb1\xf3k&\x134" + type UpdateConfigurationResponse_List struct{ capnp.List }
"\x1e\xbe\xa7y\xee?\x8e\xdb\x7f.h\xec\xec\x1aB\xa7" +
"\xa4\xd4\xd0I\x1dS\xcf\xfc\xcbU\x0f\xbfy\xae?\x08" + // NewUpdateConfigurationResponse creates a new list of UpdateConfigurationResponse.
"\xae\xaa\xa9CiC\x8d\x0dAR\xfer\xe6\x7f\x1cI" + func NewUpdateConfigurationResponse_List(s *capnp.Segment, sz int32) (UpdateConfigurationResponse_List, error) {
"D\x13\xe7\xcb\x02Xa'\xaf\xa6\x0b\xa5\x035v\xf2" + l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz)
"j\xde\"\x98\xdd\xf7\xc9\x1dK>\xbc\xf7\xeb\x0b\xe5\xb9" + return UpdateConfigurationResponse_List{l}, err
"\xb6\xb7~eL\x12\xa5\x83cl~\x19C\xc8xb" + }
"\xdeoV\x9c\xd9<\xea\x9b\xbe$\x97\xe8B\xa9'a" +
"\x93\\b\x9dt\x94~Y\x1f\x08\xcf\xd67\xafx\xe7" + func (s UpdateConfigurationResponse_List) At(i int) UpdateConfigurationResponse {
"\xdb@-\xecK\xb4\x92\xc3\x8f\x0bO\x9f\\\xf9\x8b;" + return UpdateConfigurationResponse{s.List.Struct(i)}
"\xbe\x0b:\xbc7\xf1\x199|(A\x0e/\xfb\xf2\xc9" + }
"\x9b\x1fY\xf8\xc2\xf7\xc1\xc4&V\xd3R\xb3\xa8\xaa," +
"\xa7\x17B\xe9\xeb\xdc\x9f\xe9Ii\xa5\xa0\x16\xa65\x15" + func (s UpdateConfigurationResponse_List) Set(i int, v UpdateConfigurationResponse) error {
"\xcdEL5\xb3i\xc5dI\xd6`\x144\xd5`m" + return s.List.SetStruct(i, v.Struct)
"\x88r\x8c\x0f\x01\x84\x10@T\xba\x00\xe4;y\x94s" + }
"\x1c\x8a\x88qJ\xbd\x98%\xe1\"\x1ee\x93C\x91\xe3" +
"\xe2\xc4<\xe2\xe2q\x00r\x8eGy\x09\x87\xc8\xc7\x91" + func (s UpdateConfigurationResponse_List) String() string {
"\x07\x10\x8b\x1b\x01\xe4%<\xcak8\xb4\x0aL\xcf+" + str, _ := text.MarshalList(0xdb58ff694ba05cf9, s.List)
"*S!j\xce\xd0u\xac\x06\x0e\xab\x01-\x9d\x99z" + return str
"\x8f\xd2\x9e\x83(\x0b\x88\x85\xae\xbbL\x8c\x00\x87\x11@" + }
"k\x91V\xd4\x8d\xf9\xaa\x89\xd9\\\x92u\xe8\xcc\xc0E" +
"X\x01\x1cV\x00\x0e\xe4^\x8a\x19FVSg+\xaa" + // UpdateConfigurationResponse_Promise is a wrapper for a UpdateConfigurationResponse promised by a client call.
"\xd2\xc9t\x00\xf2\xac\x92\x0f\x03x\xa4\x8d.\xbd\x8b\xf5" + type UpdateConfigurationResponse_Promise struct{ *capnp.Pipeline }
"[\x81\x13'\x0a\xe830\xba\xf0\x13\xaf\xdc\x05\x9c8" +
"V\xb0t\xd6\x995L\xa6\xe3\xfcL\xc1\xde\x9b\xd7\xd4" + func (p UpdateConfigurationResponse_Promise) Struct() (UpdateConfigurationResponse, error) {
"F\xb4\x8a\xaa\xf3\x01\x99\xee|\x88\xd2\xa9\x8d\xd8\x86\xbe" + s, err := p.Pipeline.Struct()
"u|_\xebn\xcae\x99jF[\xd4\x0e\xad,\xe4" + return UpdateConfigurationResponse{s}, err
"\xad\xfd\x85\xbc\xb5\x14\xf25\x81\x90\xaf\x9a\x0e /\xe3" + }
"Q\xbe\x9fC\x91/\xc5|m\x1d\x80\xbc\x92G\xf9A" +
"\x0e\xad\xb4}HK\x06\x00\xbchv0\xc5,\xea\xcc" + type ConfigurationManager struct{ Client capnp.Client }
" \xd9\x08\xc06\x1e\xed\xa0\x8f\x00\\\xd1\xcdt\xb2\xdd" +
"MBT\xd1\xd3\x8b\xbcD\x0d\x10\xe9\x19K\xb2\x86\x99" + // ConfigurationManager_TypeID is the unique identifier for the type ConfigurationManager.
"U;\xe7\xd9\xf2\x866-\x97M\xf7\x90W\xd5\xb6\x9d" + const ConfigurationManager_TypeID = 0xb48edfbdaa25db04
"c\xa7\x01 \x8a#o\x03@N\x14\xa7\x034d;" +
"UMgV&k\xa45Ue\xc0\xa7\xcd\x15\xedJ" + func (c ConfigurationManager) UpdateConfiguration(ctx context.Context, params func(ConfigurationManager_updateConfiguration_Params) error, opts ...capnp.CallOption) ConfigurationManager_updateConfiguration_Results_Promise {
"NQ\xd3\xcc;\xa8\xa2\xefA\xce\x01)\xa6w3}" + if c.Client == nil {
"\x92\x12\x80\xef\xf86EW\xf8\xbc!W{q\x9cq" + return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))}
"\x1b\x80\xdc\xcc\xa3\xdc\x16\x88\xe3l\x8a\xe3,\x1e\xe5[" + }
"\x03q\x9cOql\xe3Q^\xc8\xa1\xa5\xe9\xd9\xce\xac" + call := &capnp.Call{
"z\x13\x03^\x0f\"\xd00U%\xcf(f\xa5x\xac" + Ctx: ctx,
"\xd0\x0afVS\x0d\x8c\xf9\xfc\x0f\x88\xb1@\xa4\x84\xc1" + Method: capnp.Method{
"09\xc9\x85\x94\x8b(M\x1d\x9fdFQ\xc8\x99\x86" + InterfaceID: 0xb48edfbdaa25db04,
"\x1c\xf2<\x89L\x03\x90+y\x94\xe3\x1c6\xe8\xcc(" + MethodID: 0,
"\xe6L\x8c\xf9\xd7\xec_\xe2T7|\x01\x18&\xfb\x83" + InterfaceName: "tunnelrpc/tunnelrpc.capnp:ConfigurationManager",
"\xe1d\x009\xc3\xa3\\\xe0\x10K\xd1\xcbO\x0f\xb0\x01" + MethodName: "updateConfiguration",
"\x8f\x0e\x0a\x17o\x05\x90M\x1e\xe5\x95\x1cZ\x86sH" + },
"\x0b`\xc6\x8dhm\xc60[\x0a\xee_+2\x86\xd9" + Options: capnp.NewCallOptions(opts),
"\xa6\xe9&\x0a\xc0\xa1\x00\x84[\xcd`M\x1dTS-" + }
"\x99\x1c\xbb9\xcb\xab&\x86\x81\xc30\x0cXT\x0e>" + if params != nil {
"\xa2DlN\xb5\xbb\xdeL 0\xfc\x1d\x8f\xf2\xdf\x07" + call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 1}
"\xbc\xa9'\x1e\xbb\x9eG\xf9F\x0e-%\x9d\xd6\x8a\xaa" + call.ParamsFunc = func(s capnp.Struct) error { return params(ConfigurationManager_updateConfiguration_Params{Struct: s}) }
"9\x0fx\xa5\xb3\x0c\xf3)\x06\xd1\xb4\xce|8\x0c=" + }
"\xd4.9\x94\x05;\xaa+y#h^\xb2?\xf3(" + return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
"\xb0\xd7\xf2(O\xed?\x86+\xf2\xcc0\x94N\xd6\xa7" + }
"B\xc3\xfd\xb0\x0dUY\x9a\x00\x9bd\x0e\xcfO\xd2\x99" +
"!\x14s&YQmY\x8e\x19\x94\xde\xf1<\xca\xd7" + type ConfigurationManager_Server interface {
"s\x18\xc1\xef-\xc7\x8e\x89\x1b\xfd0\xd52]\xd7t" + UpdateConfiguration(ConfigurationManager_updateConfiguration) error
"\x8c\xf9\xf7`\x09}\xe9\xd2\x01\xa8\xa9\xcd\xccT\xb29" + }
"\xa4\xca\xf0\x9a\xb22\x8c\x0eV\xda~\xd8\x1c\xf1\xf8\x06" +
"\x02h\xbeWQ\x10\xc2b<\xcaWphu\xeaJ" + func ConfigurationManager_ServerToClient(s ConfigurationManager_Server) ConfigurationManager {
"\x9a\xb51\x1d\xb3Zf\x8e\xa2j)\x9e\xa5\xfb\xe0e" + c, _ := s.(server.Closer)
"\xc4\xa5\x1e\x9a\xb4K\xcd\x00o\xd5\xc0\xebuV\x0aB" + return ConfigurationManager{Client: server.New(ConfigurationManager_Methods(nil, s), c)}
"iy[\xadcs\xdc\xb3y\xf98\xff>\xf4\xd2\xbc" + }
"\xaa\xdd'l\x8f\x92\xd6\x13^\xef\xe7Q\xde\x14\xa0\xf6" +
"\x0dD^\x8f\xf2(?\xcd\xa1\x18\x0a\xc51\x04 >" + func ConfigurationManager_Methods(methods []server.Method, s ConfigurationManager_Server) []server.Method {
"I(\xd9\xc4\xa3\xbc\x9d\xeb}k\xb2n\xa6\x9a\xcd\xd9" + if cap(methods) == 0 {
"N\x10\x98\xe1K\xc9\xc4\xe6l'\x03\xde\xb8\\z\xab" + methods = make([]server.Method, 0, 1)
"\x1c$\x1eZ\xbb\xa1\xe5\x98\xc9\x9aY:\xa7\xe8\x8a\x99" + }
"\xedf\xce\xf7\x12\x18\xdd\xa4\x0e\x84\xdbd\x9f\xea!\xfc" +
"F\xddF%\x00\x87q>G\x0a,\xd0_\x0c`\xad" + methods = append(methods, server.Method{
"\xb39Y\xa6\xa9}0\xe0WL\x09\x07h\x0ct\x05" + Method: capnp.Method{
"\xfa\xeas\x0bfV\xd0T\x83\xec\x0b\xa4~Z\x7f\xa9" + InterfaceID: 0xb48edfbdaa25db04,
"\xd7\xfd\xd4\xbbt\xba~u0\xf3%:\xdd\xb0\xd5O" + MethodID: 0,
"\xb2\x18\xe2\x9c\xcco\xdb\x01 o\xe7Q~\x81\xc3\x06" + InterfaceName: "tunnelrpc/tunnelrpc.capnp:ConfigurationManager",
"\xe7\xa6\xc7\x98\xffR.e\xcb\xb9\xcffiP\x9bV" + MethodName: "updateConfiguration",
"r>\xe5Z:+\xe4\x944\x9b\x81\xa5\xbb\x1b\x10\x81" + },
"C\xb4!\x92/\xe8\xcc00\xab\xa9rQ\xc9ey" + Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error {
"\xb3\xc7\xeb\xb7\xd4b\xbeMg\xddY\xd4\x8aF\x93i" + call := ConfigurationManager_updateConfiguration{c, opts, ConfigurationManager_updateConfiguration_Params{Struct: p}, ConfigurationManager_updateConfiguration_Results{Struct: r}}
"\xb2\xbcP0\x8d\xa1tc~\x80\x88\x1f\x84l\xce(" + return s.UpdateConfiguration(call)
"c\xe8:\x9f{\xbc\x00M\xec\xf2)0Z,f=" + },
"\xee\xb3rZ\xda\xce\x1bD\xe7(\xf9\xbe\x14X1h" + ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1},
"\xad\xf6\xaat\x97\x91\xff\x9a\xba\x87\x81\x1bvr\xdd\xee" + })
"h\x03&S\x094\xf2(\xcf\x0a\x98\xdc29\xe0\x87" +
"k\xf2\xecv\xdf\x0f\xe1\xdfY\x8fkU-\xcb\x13s" + return methods
"\xbb\xc1,9\xd3\x04\xc2-\xbe\xce@\xf6\x05\x0bjn" + }
"\xa1\xd6\xf6\x90l\x9c\xea\xda(\xf5`+@j\x09\xf2" +
"\x98Z\x83\xbe\x99\xd2*\x9c\x0e\x90ZF\xf2\xfb\xd1\xb7" + // ConfigurationManager_updateConfiguration holds the arguments for a server call to ConfigurationManager.updateConfiguration.
"TZ\x8b\x09\x80\xd4J\x92?\x88\xde\xc3BZ\x8f\xbb" + type ConfigurationManager_updateConfiguration struct {
"\x00R\x0f\x92x\x0b\xa9\x87x\xbb$\xa4\xcd\xf6\xf6\x9b" + Ctx context.Context
"H\xbe\x9d\xe4\xe1P\x1c\xc3\x00\xd26\xac\x03Hm!" + Options capnp.CallOptions
"\xf9\xcb$\xaf\xe0\xe2X\x01 \xed\xc1.\x80\xd4n\x92" + Params ConfigurationManager_updateConfiguration_Params
"\xbfFr!\x1c\xa7\xb7\x95\xb4\x17u\x80\xd4\x8fH\xfe" + Results ConfigurationManager_updateConfiguration_Results
"\x06\xc9+G\xc7\xb1\x12@\xdao\xcb_'\xf9\xbb$" + }
"\xaf\x1a\x13\xc7*\x00\xe9 \xae\x06H\xbdM\xf2#$" +
"\x1f\x86q\x1c\x06 \x1d\xc6\xad\x00\xa9#$\xff%\xc9" + type ConfigurationManager_updateConfiguration_Params struct{ capnp.Struct }
"\x87W\xc4q8\x80\xf4\xa9m\xcf1\x92\x7fN\xf2\xea" +
"P\x1c\xab\x01\xa4\x13\xb8\x03 \xf59\xc9\x7fG\xf2\x88" + // ConfigurationManager_updateConfiguration_Params_TypeID is the unique identifier for the type ConfigurationManager_updateConfiguration_Params.
"\x10\xc7\x08\x80t\xd6\xf6\xeb\x0c\xc9+\xb9\xb2\xbe\xdeE" + const ConfigurationManager_updateConfiguration_Params_TypeID = 0xb177ca2526a3ca76
"TY\xf3\xcek\x86\x972V\xaaqt\xe0\xde\xa6E" +
"\xa9A\xc7\xa8?)\x03\xc4(\xa0U\xd0\xb4\xdc\x9c\xde" + func NewConfigurationManager_updateConfiguration_Params(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Params, error) {
"H\x8d\x9aJ\xa7\xe1>\x14b\xfe\xf0\x02\x90\x84\xde\xbd" + st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1})
"\x0fQMm\xc9xDP\xce:\xae%Y\xa3\xa9h" + return ConfigurationManager_updateConfiguration_Params{st}, err
"j\xc5\x02\xd4f\x14\x93e<\xce\xd1\x8b\xeaL]\xcb" + }
"\xcfC\xa6\xe7\xb3\xaa\x92\x1b\x84\x8d\xaa\x80\xc3*(Q" +
"\x82\xbb\xf7\xc0\xd4t\xf1g\x8f\x87h\xae\x1c\xd1\xb5\x85" + func NewRootConfigurationManager_updateConfiguration_Params(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Params, error) {
"i\xf3\x94\xce\xa1\xf0\xd4d\xbf\x7f\x8b\xaa\x01B\xaa\xed" + st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1})
"Vr\xc5\x1fBO\xbd[\x89d\x83\xd3\x8a\x0c\xf6(" + return ConfigurationManager_updateConfiguration_Params{st}, err
"pg\x19\x83SI\xef\x86\xb0\xf7\x85\x8a\x811#\x9d" + }
"\xc3\x95\xf6\x1f\xb2\xf9\x9d\xcct~\xd1\xeb\x96\xde\x16B" +
"\xf0\x9a\xbf\xb4\xd5IfD\x87\xe2\xba?\xf3\x19\xfc=" + func ReadRootConfigurationManager_updateConfiguration_Params(msg *capnp.Message) (ConfigurationManager_updateConfiguration_Params, error) {
"\xd4\xcf\xc5\xdf\xcf\xb5\xef\xf6\x9c\x817\x11\xe5~!\x8f" + root, err := msg.RootPtr()
"\xf2\xa2@\xeeYk?o\xa2\xa4?\x0c\x11y\xae4" + return ConfigurationManager_updateConfiguration_Params{root.Struct()}, err
"\x0d\xa1\x8b\xa2\xc0\xa3\xbc\x8c\xc3(=^1\xe6\x0f\x87" + }
"{\x19\xdd\xfb\xc1NPhQ3\x0cp\x89\x8b\xe6\xc0" +
"\xf5\xe1\x8dI\x07\xef\xce\x86\xe6\xb6\xdb\xf5\x0e\x1apo" + func (s ConfigurationManager_updateConfiguration_Params) String() string {
"\xf4Xv\xf2E\xdfe\x0d\xce\xa1\x84\xb3\xd1\xf6\x1c\xc6" + str, _ := text.Marshal(0xb177ca2526a3ca76, s.Struct)
"\x1d\xc3\xa2;\xd0\x13\xf7,\x05N\xdc)\xa0?\xaaD" + return str
"w2)n\xd3\x81\x137\x0b\xc8y\x83mt\x07\xd8" + }
"\xe2\xfa\x07\x80\x13\xd7\x0a\xc8{sitGb\xf5=" +
"\xc3\x108q\xb9\x80!o\xde\x8f\xee@M\\\xdc\x05" + func (s ConfigurationManager_updateConfiguration_Params) Version() int32 {
"\x9c\x98\x150\xec\x8d\xbc\xd1\x9d\xb9\x8a\xb7\xaf\x06N\x9c" + return int32(s.Struct.Uint32(0))
"\xef\x0f~\xa0\xc1\xf1\xa3\x11-\x17\xa3Pk\xa3\xb4\xf7" + }
"\x18\xc8\xd1\x02hD\xcb\xed\x81\xf9\x8b5\xc1\xb6\x96;" +
"\xc9\x80hZ1Y#5gN\xfdc\x89\x00\xa0\x11" + func (s ConfigurationManager_updateConfiguration_Params) SetVersion(v int32) {
"\xe5\x10\x06\xe6\x89\x00\x97\xfb\x08M\xb2Z;\xcf?\xb4" + s.Struct.SetUint32(0, uint32(v))
"er\xd7\xff@J\xe2\xfb\xb3\x9a\xce\xf1&b\x81}" + }
"\xa9\x0b\xac\xe6Q\x1e\xcd\x0d\xda\xf8\x85.\xe6\x85\x0b\xfe" +
"(-\xa6\xfd\xff\xd6\xdb\xff05N\xef\xf2(\x1f\x0b" + func (s ConfigurationManager_updateConfiguration_Params) Config() ([]byte, error) {
"\x94\xf5Q\x12~\xc0\xa3|<\xd08}D\xb5~\x8c" + p, err := s.Struct.Ptr(0)
"G\xf9\xbc?\xe4\xfc\xea\x01\x00\xf9<\x8f\xc9@#\"" + return []byte(p.Data()), err
"\xfe\x89\x14\xbf\xa3\xeb\xdanC\xd0iC\xc2\xb8\x11 " + }
"UI\xd7x\xdcnCBN\x1b\"b;@*F" +
"\xf2+\x82m\xc8\x18\xbc\x0d 5\x9a\xe4\xe3\xb1\xf7\xbb" + func (s ConfigurationManager_updateConfiguration_Params) HasConfig() bool {
"F(\xea~\xa3\x96\xd3:ge\xd5~\xef6w\xea" + p, err := s.Struct.Ptr(0)
"\x8a\xe6L%\x9b+\xea\x0c\xfc\xab\xb5D6\xcd\x81\xdb" + return p.IsValid() || err != nil
"\xde\x19\xc7:\x93\x97\x14\x810\x83\x867\x95\xb9\x84\x17" + }
"\xe5\x90n\x9e\x19\xba\xae\xa1^\xd6\xc4N\xf6\x9bX\xaf" +
"\x87\xa5^\xfcf\x1e\xe5y\x94\x8aF'\x15r\xbb\xdf" + func (s ConfigurationManager_updateConfiguration_Params) SetConfig(v []byte) error {
"v\xd7\xa6\x95\xa2\xc1\xfa\xf8\x00<\xd3\xbd)\x80\xb1H" + return s.Struct.SetData(0, v)
"+\xe62I\x06\x82\xa9\xf7\x94\x85`\xd0f6\xc5\xa2" + }
".s9\x13d\xf7\xbf!\xe8\xfe\xd3#0Av\xc7" +
"\xf8\xe8\xfeo\xab\xef\x04\xd9\x8dA\x9f\x09\xb2\xf3\xc1\xc6" + // ConfigurationManager_updateConfiguration_Params_List is a list of ConfigurationManager_updateConfiguration_Params.
"h\xef\x09\xf2e<_\x9dk,\xc0\x18\x974X\x1d" + type ConfigurationManager_updateConfiguration_Params_List struct{ capnp.List }
"\xf2<\xd2\xfb\xf7oY\xa5W]\xee\x98\xc0\xbd\x90\xfe" +
"\x1c\x00\x00\xff\xff\xa1\x1ap\xe9" // NewConfigurationManager_updateConfiguration_Params creates a new list of ConfigurationManager_updateConfiguration_Params.
func NewConfigurationManager_updateConfiguration_Params_List(s *capnp.Segment, sz int32) (ConfigurationManager_updateConfiguration_Params_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz)
return ConfigurationManager_updateConfiguration_Params_List{l}, err
}
func (s ConfigurationManager_updateConfiguration_Params_List) At(i int) ConfigurationManager_updateConfiguration_Params {
return ConfigurationManager_updateConfiguration_Params{s.List.Struct(i)}
}
func (s ConfigurationManager_updateConfiguration_Params_List) Set(i int, v ConfigurationManager_updateConfiguration_Params) error {
return s.List.SetStruct(i, v.Struct)
}
func (s ConfigurationManager_updateConfiguration_Params_List) String() string {
str, _ := text.MarshalList(0xb177ca2526a3ca76, s.List)
return str
}
// ConfigurationManager_updateConfiguration_Params_Promise is a wrapper for a ConfigurationManager_updateConfiguration_Params promised by a client call.
type ConfigurationManager_updateConfiguration_Params_Promise struct{ *capnp.Pipeline }
func (p ConfigurationManager_updateConfiguration_Params_Promise) Struct() (ConfigurationManager_updateConfiguration_Params, error) {
s, err := p.Pipeline.Struct()
return ConfigurationManager_updateConfiguration_Params{s}, err
}
type ConfigurationManager_updateConfiguration_Results struct{ capnp.Struct }
// ConfigurationManager_updateConfiguration_Results_TypeID is the unique identifier for the type ConfigurationManager_updateConfiguration_Results.
const ConfigurationManager_updateConfiguration_Results_TypeID = 0x958096448eb3373e
func NewConfigurationManager_updateConfiguration_Results(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Results, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return ConfigurationManager_updateConfiguration_Results{st}, err
}
func NewRootConfigurationManager_updateConfiguration_Results(s *capnp.Segment) (ConfigurationManager_updateConfiguration_Results, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1})
return ConfigurationManager_updateConfiguration_Results{st}, err
}
func ReadRootConfigurationManager_updateConfiguration_Results(msg *capnp.Message) (ConfigurationManager_updateConfiguration_Results, error) {
root, err := msg.RootPtr()
return ConfigurationManager_updateConfiguration_Results{root.Struct()}, err
}
func (s ConfigurationManager_updateConfiguration_Results) String() string {
str, _ := text.Marshal(0x958096448eb3373e, s.Struct)
return str
}
func (s ConfigurationManager_updateConfiguration_Results) Result() (UpdateConfigurationResponse, error) {
p, err := s.Struct.Ptr(0)
return UpdateConfigurationResponse{Struct: p.Struct()}, err
}
func (s ConfigurationManager_updateConfiguration_Results) HasResult() bool {
p, err := s.Struct.Ptr(0)
return p.IsValid() || err != nil
}
func (s ConfigurationManager_updateConfiguration_Results) SetResult(v UpdateConfigurationResponse) error {
return s.Struct.SetPtr(0, v.Struct.ToPtr())
}
// NewResult sets the result field to a newly
// allocated UpdateConfigurationResponse struct, preferring placement in s's segment.
func (s ConfigurationManager_updateConfiguration_Results) NewResult() (UpdateConfigurationResponse, error) {
ss, err := NewUpdateConfigurationResponse(s.Struct.Segment())
if err != nil {
return UpdateConfigurationResponse{}, err
}
err = s.Struct.SetPtr(0, ss.Struct.ToPtr())
return ss, err
}
// ConfigurationManager_updateConfiguration_Results_List is a list of ConfigurationManager_updateConfiguration_Results.
type ConfigurationManager_updateConfiguration_Results_List struct{ capnp.List }
// NewConfigurationManager_updateConfiguration_Results creates a new list of ConfigurationManager_updateConfiguration_Results.
func NewConfigurationManager_updateConfiguration_Results_List(s *capnp.Segment, sz int32) (ConfigurationManager_updateConfiguration_Results_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz)
return ConfigurationManager_updateConfiguration_Results_List{l}, err
}
func (s ConfigurationManager_updateConfiguration_Results_List) At(i int) ConfigurationManager_updateConfiguration_Results {
return ConfigurationManager_updateConfiguration_Results{s.List.Struct(i)}
}
func (s ConfigurationManager_updateConfiguration_Results_List) Set(i int, v ConfigurationManager_updateConfiguration_Results) error {
return s.List.SetStruct(i, v.Struct)
}
func (s ConfigurationManager_updateConfiguration_Results_List) String() string {
str, _ := text.MarshalList(0x958096448eb3373e, s.List)
return str
}
// ConfigurationManager_updateConfiguration_Results_Promise is a wrapper for a ConfigurationManager_updateConfiguration_Results promised by a client call.
type ConfigurationManager_updateConfiguration_Results_Promise struct{ *capnp.Pipeline }
func (p ConfigurationManager_updateConfiguration_Results_Promise) Struct() (ConfigurationManager_updateConfiguration_Results, error) {
s, err := p.Pipeline.Struct()
return ConfigurationManager_updateConfiguration_Results{s}, err
}
func (p ConfigurationManager_updateConfiguration_Results_Promise) Result() UpdateConfigurationResponse_Promise {
return UpdateConfigurationResponse_Promise{Pipeline: p.Pipeline.GetPipeline(0)}
}
type CloudflaredServer struct{ Client capnp.Client }
// CloudflaredServer_TypeID is the unique identifier for the type CloudflaredServer.
const CloudflaredServer_TypeID = 0xf548cef9dea2a4a1
func (c CloudflaredServer) RegisterUdpSession(ctx context.Context, params func(SessionManager_registerUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_registerUdpSession_Results_Promise {
if c.Client == nil {
return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))}
}
call := &capnp.Call{
Ctx: ctx,
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 0,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "registerUdpSession",
},
Options: capnp.NewCallOptions(opts),
}
if params != nil {
call.ParamsSize = capnp.ObjectSize{DataSize: 16, PointerCount: 2}
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_registerUdpSession_Params{Struct: s}) }
}
return SessionManager_registerUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
}
func (c CloudflaredServer) UnregisterUdpSession(ctx context.Context, params func(SessionManager_unregisterUdpSession_Params) error, opts ...capnp.CallOption) SessionManager_unregisterUdpSession_Results_Promise {
if c.Client == nil {
return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))}
}
call := &capnp.Call{
Ctx: ctx,
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 1,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "unregisterUdpSession",
},
Options: capnp.NewCallOptions(opts),
}
if params != nil {
call.ParamsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 2}
call.ParamsFunc = func(s capnp.Struct) error { return params(SessionManager_unregisterUdpSession_Params{Struct: s}) }
}
return SessionManager_unregisterUdpSession_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
}
func (c CloudflaredServer) UpdateConfiguration(ctx context.Context, params func(ConfigurationManager_updateConfiguration_Params) error, opts ...capnp.CallOption) ConfigurationManager_updateConfiguration_Results_Promise {
if c.Client == nil {
return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(capnp.ErrorAnswer(capnp.ErrNullClient))}
}
call := &capnp.Call{
Ctx: ctx,
Method: capnp.Method{
InterfaceID: 0xb48edfbdaa25db04,
MethodID: 0,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:ConfigurationManager",
MethodName: "updateConfiguration",
},
Options: capnp.NewCallOptions(opts),
}
if params != nil {
call.ParamsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 1}
call.ParamsFunc = func(s capnp.Struct) error { return params(ConfigurationManager_updateConfiguration_Params{Struct: s}) }
}
return ConfigurationManager_updateConfiguration_Results_Promise{Pipeline: capnp.NewPipeline(c.Client.Call(call))}
}
type CloudflaredServer_Server interface {
RegisterUdpSession(SessionManager_registerUdpSession) error
UnregisterUdpSession(SessionManager_unregisterUdpSession) error
UpdateConfiguration(ConfigurationManager_updateConfiguration) error
}
func CloudflaredServer_ServerToClient(s CloudflaredServer_Server) CloudflaredServer {
c, _ := s.(server.Closer)
return CloudflaredServer{Client: server.New(CloudflaredServer_Methods(nil, s), c)}
}
func CloudflaredServer_Methods(methods []server.Method, s CloudflaredServer_Server) []server.Method {
if cap(methods) == 0 {
methods = make([]server.Method, 0, 3)
}
methods = append(methods, server.Method{
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 0,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "registerUdpSession",
},
Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error {
call := SessionManager_registerUdpSession{c, opts, SessionManager_registerUdpSession_Params{Struct: p}, SessionManager_registerUdpSession_Results{Struct: r}}
return s.RegisterUdpSession(call)
},
ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1},
})
methods = append(methods, server.Method{
Method: capnp.Method{
InterfaceID: 0x839445a59fb01686,
MethodID: 1,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:SessionManager",
MethodName: "unregisterUdpSession",
},
Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error {
call := SessionManager_unregisterUdpSession{c, opts, SessionManager_unregisterUdpSession_Params{Struct: p}, SessionManager_unregisterUdpSession_Results{Struct: r}}
return s.UnregisterUdpSession(call)
},
ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 0},
})
methods = append(methods, server.Method{
Method: capnp.Method{
InterfaceID: 0xb48edfbdaa25db04,
MethodID: 0,
InterfaceName: "tunnelrpc/tunnelrpc.capnp:ConfigurationManager",
MethodName: "updateConfiguration",
},
Impl: func(c context.Context, opts capnp.CallOptions, p, r capnp.Struct) error {
call := ConfigurationManager_updateConfiguration{c, opts, ConfigurationManager_updateConfiguration_Params{Struct: p}, ConfigurationManager_updateConfiguration_Results{Struct: r}}
return s.UpdateConfiguration(call)
},
ResultsSize: capnp.ObjectSize{DataSize: 0, PointerCount: 1},
})
return methods
}
const schema_db8274f9144abc7e = "x\xda\xccZ{t\x14\xe7u\xbfwfW#\x81V" +
"\xab\xf1\xac\xd1\x03T\xb5:P\x179\xd8\x06Jk\xab" +
"9\xd1\xc3\x12\xb1d\x03\x9a]\x94\xe3cC\x8eG\xbb" +
"\x9f\xa4Qwg\x96\x99Y\x19\x11\x130\x01c\xfb\xb8" +
"\x8eq\xc0\xb1Ih0.\xed\x01\xdb\xad\x89\xdd\xa6\xee" +
"1\xa7\xa6\xcd\xabq\xc0&\x87\xf4\x90@\x9a&\x84>" +
"8\xb8\xae14\x876\xf1\xf4\xdc\x99\x9d\x87v\x17\x09" +
"\x8c\xff\xc8\x7f\xab;\xdf\xe3\xde\xdf\xf7\xbb\x8f\xef~\xba" +
"\xed\xe6\x9a.nq\xf4\xed:\x00\xf9\xa5h\x95\xcd\xda" +
"\x7f\xb0a\xef\x82\x7f\xdc\x02r3\xa2\xfd\xf97\x06\x12" +
"\x97\xad-\xa7 \xca\x0b\x00KW\x08\x1bPR\x04\x01" +
"@Z+\xfc;\xa0\xfd\xc8\x9cW\xbe\xb6\xbfo\xe7\x17" +
"@l\xe6\x83\xc1\x80K\xbb\xab\x07P\x1a\xaa\xa6\x91r" +
"\xf5v\xe9\x10\xfd\xb2\xef\x16o\xbd?\xf1\xce1\x1a\x1d" +
"^:BK?W\xdd\x8e\xd2\x01g\xc2\xfejZ\xfa" +
"\x93\xb9\xb7\xf7\xfd\xc1\xae\xb7\xb6\x82\xd8\xccMYzG" +
"\xcd\x06\x94\xf6\xd7\xd0\xc8\xe7kV\x01\xda\x1f\xecl|" +
"\xf1\xf9c\xdf\xdd\x06\xe2M\x08EM_\xaf\xf91\x02" +
"JGk\xfe\x0a\xd0>z\xe9\xfe\x8b\xaf}{\xd9#" +
" .\xa4\x01H\x03r\xb3\xda8@i\xdb\xacN@" +
"\xfb\xdc\xf9\xff\xdb\xfe\xb9\x85+\x9f\x02y!r\x00Q" +
"\x8eF\xec\x9f\xd5L#\x0e\xcf\"m:\x97\x1f}\xbd" +
"y\xe93;KTw\x06\xee\x99\xdd\x8e\xd2\xcb\xb3I" +
"\xa1\x03\xb3\x1f\x04\xb4?\xf5\x87\xaf>\xd9\xfb\xcc\xe6]" +
" \xde\xea\xef\x17\xab\xbd\x8fV[XK\xfb\xfdO\xdd" +
"W\x8e\x15\xee\xfc\xc63E\x85\x9cU\xfak\xdbi\x80" +
"RK+\xb4M,x\xe0\x1f\xbe\xf5\xea\x97A^\x84" +
"h\x9f\x1e\xbe\xf9\x87\xfc\x9e\x83\xa7`\x08\x05\xd2o\xe9" +
"\x91\xda}d\xddqg\xec\xdb\x9fx\xe3\xef\x9ezu" +
"\xfbW@\xbe\x09\x11\xc0AsY\xec\x7fi@\x7f\x8c" +
"v\xdby\xf2\xf0\xca\xdc\x8e\xdd\xfb\\|\x9c\xef\xebb" +
"\x1c\x07\x11{k\xff/sC/\xa4^(\"\x17\xa5" +
"O,v\x01\x01\x97N\xc6Z\x11\xd0^\xf6\xe3\xb3\xab" +
"V|}\xe4/Bsw\xd5m\xa0\xb9\xdbG.\x1c" +
"\xa9O\xe6^,A\xc41vG\xddA\x94\x0e\xd49" +
"\x87YG*\xbc\xfc[w\xd7\xac?\xbb\xfc\x15\x10\x17" +
"y\xcb|\xab.I\xcbL|\xef\x85\xdf]\xf0\xbd\x07" +
"\x0f\x81|+\xfa`\x1d\xa1o(\xfd\xa4\x8e\xec\x8b\x9c" +
"Zp\xf0\xf0O\x9f|\xad\x8ccw\xc47\xa0\xb4\"" +
"N\xbb\xf4\xc7?-M\xd2/;\xb2\x96\xffPy\xf6" +
"\xef_+\xe5\xaf\x83\xb1\x12\x1fF\xa9\x10w\x10\x88;" +
"\xf6=~d\xf7\xcd\xd5_\xfb\xe0\xaf+\x9d\xebs\xf5" +
"\xc3(\xbd\\\xef\x9ck=irc?\x9e~sq" +
"\xe4\x1ba\xa2\xd5\x88\xe7\x08\xe9\x16\x91\x88\xd6\xf2nO" +
"L{o\xcb\x9b%\xab9\x03\x0f\x8b\x03(\x1d\x17i" +
"\xb5\xa3\xce\xe0\x81\xfb\xbf\xf4t\xf4\xec\x97\xbeC\x9a\x86" +
"\x18\x1e%\x1fX\xaa\xde`\xa0\xb4\xf1\x06\xfa9yC" +
"\x03\x0fh7\xbf\xf2G\x7f\xd9\x93\xf9\xd1[\x154\x95" +
".\xdfxA\x8a\xce\xa1_8\x87\x14=\xb3\xe8\xd0\xe7" +
"\xfe\xf3O\x8e\x9f(*\xea`\xbav\x8eC\x89us" +
"\xe8<.\xaf\xd9{\xb7j\xdf{\xaa\x14%\xf7\xf4\xe6" +
"|\x1d\xa5\x03\xcer\xfb\x9d\xe5|\xfeU\x1a]\xd30" +
"\x8eRK\x03\x8dnj\xa0\xb5\xb9\xb3J\xd3\xe6\x7f\xfe" +
"\xd4\xe9\x10eZ\x1a~\x8e\x10\xb1W~\xe6\xfe\xf1\x9a" +
"\x8dg\xce\x84\xd5\x12\x1b\x1c\xfc\x168S\xff\xeb\xcf\xcf" +
"}\xf1|.\xf3o\x0e\xed=\x84\xfb\x1a:\x88\x0ck" +
"\x1b\xc8\x0f\x1bZc}m'\x07\xcf\xb9Dr\x97\xb8" +
"\xa3\xb1\x87\x06\xc8\x8d\xb4\xc4\xb2\x07\xba\xd9\x9a\xdb\xef=" +
"W\xc6\x96u\x8d\x1d(=\xdcH\x1366nGi" +
"WS\x03\x80=\xf17;\xee}\xf1\x9b+/\xb8\x9e" +
"\xe8(\xbb\xadi\x09\x11\xf3\xc9\xcf\xf7\xae\xba\xa3\xed\xc8" +
"\x85\xb0\xb2\x1b\x9b\xc87\xa4\x1dM\xb4\xd3\xc8\xed\xe7?" +
"\xbd\xe0\xc9o_\xa8\xe4\x00\x87\x9a\xdaQ:\xd2D\xa0" +
"\x1c\xa6\xc1\xef-\xff\xd3\x13\xcd\xf1\xe6\x8b%\x00V\xd1" +
"\xd8\x9f5\x8d\xa3t\x89\xc6.}\xbf\xe9;D\xca\xe7" +
"\xffl\xdf\xbf\\>v\xd7\xa52\x1b\xce\xce\x1dF\xe9" +
"\xf2\\Z\xf6\xd2\\A\xba4\xf7&\x00\xfb\x91S\x9f" +
"]\xff\x83/|p\xa9\x94G\x8e\"\xef\xceM\xa2\x84" +
"\xf3h\xc6\xaf\xe7\x12\xeb\xbe\xbc\xfa?6\x9d\xdf5\xe7" +
"\x97ek\xef\x997\x8e\xd2!g\xe4\xcb\xf3\xb6K\xb1" +
"\x16\xf2\xa6w\x84\x17\x16\xf7nz\xebr\xc8o/\xcd" +
"\x1b x\x9e\x11\xbezf\xf3O?\xfb\xab0<\xef" +
"\xcf\xfb9\xc1\x13m!x\x1ez\xef\xb9\xbb\xbe\xb8\xe6" +
"\xa5\x0fC4X\xd0\xb2\x85\xa6Z\x05McY#\x1f" +
"I\xdf\xea\xfdL\xdf\x92V\xf2Z\xbe\xa3\xbb`\x8d1" +
"\xcdR\xd3\x8a\xc5\x92\xac\xd3\xcc\xeb\x9a\xc9\x06\x11\xe5z" +
">\x02\x10A\x00Q\x19\x07\x90\x1f\xe0Q\xcer(\"" +
"&\x88(\xa2J\xc21\x1ee\x8bC\x91\xe3\x12\x14%" +
"\xc5um\x00r\x96Gy=\x87\xc8'\x90\x07\x10\x0b" +
"O\x03\xc8\xeby\x94\xb7rh\xe7\x99\x91S4\xa6A" +
"\xdc\xea3\x0c\xac\x05\x0ek\x01m\x83Y\xc6\xa42\x9c" +
"\x858\x0b\x89\x85\xf1\x07-\x8c\x01\x871@{L/" +
"\x18\xe6\x90f\xa1\x9aM\xb2\x11\x83\x998\x86U\xc0a" +
"\x15\xe0t\xe6\xa5\x98i\xaa\xba\xb6B\xd1\x94Qf\x00" +
"\x90e\xd5|\x14\xc0\xcf@\xe8\xe5*q\xf1n\xe0\xc4" +
"E\x02\x06\xd9\x02=\xb2\x8a\xbfs\x108\xb1E\xb0\x0d" +
"6\xaa\x9a\x163p(\x93w\xd6\xe6u\xad\x0b\xed\x82" +
"\xe6~@f\xb8\x1f\xe2\xb4k\x17\x0eb\xa0\x1d_\xae" +
"\xdd\x9dY\x95iV\xbc_\x1b\xd1K \x1f\xa8\x04\xf9" +
"@\x11\xf2\xad!\xc8\x1f\xee\x01\x90\x1f\xe2Q~\x94C" +
"\x91/b\xbe\xad\x1d@\xde\xcc\xa3\xfc\x04\x87v\xda\xd9" +
"\xa4?\x03\x00>\x9a#L\xb1\x0a\x063IV\x078" +
"\xc8\xa3\x03z\x1d\xe0\xa6\x09f\x90\xee\xde!\xc4\x15#" +
"=\xe6\x1f\xd44H\xf7\xadWMK\xd5FW;\xf2" +
"\xceA=\xab\xa6'\xc9\xaaZG\xcf\x96\x0e\x00D\xf1" +
"\xc6\xfb\x00\x90\x13\xc5\x1e\x80NuT\xd3\x0dfgT" +
"3\xadk\x1a\x03>mm\x1aV\xb2\x8a\x96f\xfeF" +
"U\xe5\x1b\xb9\x1b\xa4\x981\xc1\x8c[\x94\x10}\xe7\x0f" +
"*\x86\xc2\xe7L\xb9\xd6\xc7\xb1\xef>\x00\xb9\x97Gy" +
"0\x84\xe3\x0a\xc2\xf1\x1e\x1e\xe5{C8\x0e\x11\x8e\x83" +
"<\xcak8\xb4uC\x1dU\xb5;\x19\xf0F\x98\x81" +
"\xa6\xa5)9F\x98\x15\xf1\xd8\xa4\xe7-U\xd7L\xac" +
"\x0fr\x0b \xd6\x87\x90\x12f\xe2\xe4-\x1e\xa5<F" +
"\xe9\xda\xfc$3\x0bB\xd62\xe5\x88oI\xac\x03@" +
"\xae\xe6QNp\xd8i0\xb3\x90\xb5\xb0>(\x09>" +
"\x8e]=\xf8B4LV\xa2\xe1\x12\x009\xc3\xa3\x9c" +
"\xe7\x10\x8b\xe8\xe5zB\xd1\x80G\x97\x85\xebv\x03\xc8" +
"\x16\x8f\xf2f\x0em\xd3\xdd\xa4\x1f0\xe3!\xda\x9a1" +
"\xad\xfe\xbc\xf7\xd7\xa6\x8ci\x0d\xea\x86\x85\x02p(\x00" +
"\xf1V7Y\xf7\x08\xf9T\x7f&\xcb\xeeRy\xcd\xc2" +
"(p\x18\x85i\x9d\xca\xe5G\x9c\x02\x9b\xeb\xed\x9e5" +
"\x0b\x89\x0c\xbf\xc7\xa3\xfc\xfb!k\x16S\x1c\xbb\x8dG" +
"\xf9\x93\x1c\xdaJ:\xad\x174k5\xf0\xcah\x09\xe7" +
"S\x0c\xe2i\x83\x05t\xf0\xb6\xad\xae\xe0\xd6\xba6\xa2" +
"\x8e\x16\x0c\xc5\x0a\x01^\xc8g\x14\x8bM\xf9\xe4\x9cs" +
"\x96\xbf\x8as\xf6\xab\x87k>g/2\x95\x9ct\xdc" +
"Prf\x18\x9bd%l\xe8T?\xc1\xa3|{\xe5" +
"\x03\xdc\x94c\xa6\xa9\x8c\xb2\xb2\xf0\x10\xad\x88\x89\xc6\xd2" +
"du\x92\xb9I\xe6\x16\x83\x99B!k\x91\x16\xb5\xb6" +
"\xed\xaaA\xdc\x9a\xcf\xa3|\x1b\x871\xfc\xd0v\xf5X" +
"\xf4tpF\xad\xcc0t\x03\xeb\x83$\\\x84$]" +
"\xdc\x00u\xad\x97Y\x8a\x9aErK\xbf\xda,\x01n" +
"\xa6\xb8\x12\xc0\xe6\x8a\xe7w\x92w\xe4\xa6\x9c\x14\xd1\xbb" +
"\x9eGy\x1e\x87\xf6\xa8\xa1\xa4\xd9 3P\xd53+" +
"\x15MO\xf1,]F\xd6\xbak\xdd\xd4\xe1\x87e\x82" +
"?k\xfa\xf9\x06+\x82P\x9c>\xd8\xea\xea\x9c\xf0u" +
"\xde\xd8\x16$c\xff\x98\x1f\x1e\x0e\xb2\x85\x1f\x0f\x1f#" +
"gy\x94Gyg(\xaf\xec\xa0\xc8\xf9\x14\x8f\xf2W" +
"9\x14#\x91\x04F\x00\xc4\xe7\x88%;y\x94\xf7r" +
"SS6\x9b`\x9a\xd5\xab\x8e\x82\xc0\xcc@J*\xf6" +
"\xaa\xa3\x0cx\xf3zck\xf5\x0cx\xe8\xc3\xa6\x9ee" +
"\x16\xebe\xe9\xacB.7\xc1\xdc\xefE2z\x87:" +
"\x1do\x93e\xdeC\xfc\x8d{UR\x88\x0em\x81\xe3" +
"\x0a,T\xdcL\xa3\xad\xbb\xb8\x1b\x0c\xca8\x10xL" +
"\x91\x07h~,A\xc7\xb1\x19\xa78\x7fO\xe0u\x1e" +
")\x16u\x04\x01\xc1\xaf\x09\"\xc0a\x04\xb03\xed," +
"X\x16\x0a#3i\xd5\xe9\xaa\xe5\x02GE\x98w\x17" +
"E\xef\x02/\x8a\xfb\x80\x13c\x82\xedi\x8e\xde|\xa1" +
"\xac\xa0\x8aL\x17eV\xe5-U\xd05\x93\xf6\x0a\xf1" +
"\xbf\xa3\x12\xff\x8d\x80\xff^B{lK\x98\xfe\xc5\x84" +
"\xb6cw\xc0t1\xc2\xb9\xf4\xdf\xb3\x0f@\xde\xcb\xa3" +
"\xfc\x12\x87\x9dn\xad\x85\xf5A\xe3\xa5HY\xb7\xa2\xb8" +
"G\x87\xd6\xb4\x92\x0d\x92\x9em\xb0|VI\xb3>," +
"VO\x80\x08\x1c\xa2\xe3'\xb9\xbc\xc1L\x13U]\x93" +
"\x0bJV\xe5\xadI\xbf\xe2\xd5\x0a\xb9A\x83M\xa8\xa8" +
"\x17\xccn\xcbb9!o\x99WS\x0f\x07\x00Q\x90" +
"\x14\xd4\xacY\x92#\xdb\x03*\xf8\x00-\x1a\x0f\xf2@" +
"\xbcPP\xfd\x04`g\xf5\xb4s\xb2\x10_\xa9\xe4\xca" +
"\xf3@\xd5\x8c\x01kJ\xb8\xf3\xd2\xd2oR\xfd6\xfd" +
"\x95\x89Lw\xee\x14!\x95)\x0et\xf1(\xdf\x13R" +
"\xb9\x7fI\xc8\x0eO\xe5\x15\xc3\x81\x1d\xc2\x1f\xb3IO" +
"\xabV\x96\xa3\xf4\xe5\x81Y4\xa6\x1b\x84\xbb\x831\xd3" +
"\xe9\x17\x8e*\xab\xf2\xad\x8e\x85\xa4\xe3\xed\x9e\x8e\xd2$" +
"\x0e\x00\xa4\xd6#\x8f\xa9\xad\x18\xa8)=\x8c=\x00\xa9" +
"\x87H\xfe(\x06\x9aJ\xdb\xb0\x19 \xb5\x99\xe4O\xa0" +
"\x7f\xb5\x93\x1e\xc3\x83\x00\xa9'H\xfc,\x0d\x8f\xf0\x8e" +
"KH\xbb\x9c\xe5w\x92|/\xc9\xa3\x91\x04F\x01\xa4" +
"=\xd8\x0e\x90z\x96\xe4\xaf\x91\xbc\x8aK`\x15\x80t" +
"\x08\xc7\x01R\xaf\x90\xfc\x0d\x92\x0b\xd1\x04\xddn\xa5\xd7" +
"\xd1\x00H\xfd-\xc9\xbfI\xf2\xea\xc6\x04V\x03HG" +
"\x1c\xf9\x9b$\xff>\xc9k\x9a\x12X\x03 \xfd\x13n" +
"\x01H}\x97\xe4'H>\x0b\x138\x0b@:\x8e\xbb" +
"\x01R'H\xfe\xaf$\x9f]\x95\xc0\xd9\x00\xd2O\x1c" +
"}N\x92\xfc\x17$\xaf\x8d$\xb0\x16@\xfa\x19\xee\x03" +
"H\xfd\x82\xe4\xffM\xf2\x98\x90\xc0\x18\x80\xf4\xaec\xd7" +
"y\x92Ws%7+\x8fQ%\xd7'^7\xfd#" +
"cE\x1fG\x97\xee\x83z\x9c\xaeH\x18\x0f\x1a\xaf\x80" +
"\x18\x07\xb4\xf3\xba\x9e]9\x95\xa9qK\x195\xbd\xab" +
"Z}\xd0\x9a\x02$\xa1_\xfc@\\\xd7\xfa3~ " +
"(\x8d:\x9e&\xaa\xd9]\xb0\xf4B\x1eZ)\xc8f" +
"\xfc\x98c\x14\xb4\xe5\x86\x9e[\x8d\xcc\xc8\xa9\x9a\x92\x9d" +
"!\x1a\xd5\x00\x875P\x0c\x09\xde\xda\xd3\x87\xa6+_" +
"<}Fs\xa5\x8cn\xcdw\xacVF\xaf&N-" +
"\x09rV\\\x0b\x05\xa4\xd6\x09%[\xf8(\xe1ij" +
"=\x95\xect\xeb\xb1\x99\xcau\xaf\xf7T\x12J*T" +
"\x17C\xe5\xf99\xc9\xccV\xbf\x09\x132\xf8`\x10\x83" +
"={\x97\xb5\x85\xee.Y\xc5b\xa6\xd5\x9d\xc7|V" +
"e\x99\xcf0#\x1eN\xd9\x15+\x92\xc8Le\xfa\xd4" +
"2\x07C]r2\x9c+\x1a|\xd5x\x8e2\xcb\xfd" +
"\xd5\xaf\x8d\xe8T\x87\x08\xe1\xe2\xeb\xdaf'\x99\x19\xbf" +
"\x9a\xb3\x08\x9a\x863_\x9d*\x94c\x15\x8a1\xef&" +
"\x10\xba&\x13\x19\xd7\xf0(\x8f\x85\xc8\xc8\x06*\\\x93" +
"\x93A\x7fL\xe4\xb9b\x83\x8c2W\x9eG\xf9!\x0e" +
"\xe3J\xc1\x1a\xc3\xfa\xe0\xf1c\x8a\xd2S{8\xc4\xcd" +
"~-\xc3\x00\xd7{\xee\x15\xcag~W~\xe6\x9a\xf9" +
"\xea\xcc\xf6\xee\"3\x02\xee\xf7\xaeKv\xbe\xe2U\xbd" +
"\xd3\xdd\x94x\xd6\xe8T\x85^\xd7\x1f\xbd\x8e\xb0xh" +
"\x03p\xe2\x01\x01\x83^7z\xadmq\x8f\x01\x9c\xb8" +
"K@\xce\x7f\x97A\xef\xfdE|\xecq\xe0\xc4m\x02" +
"\xf2\xfe\xb3\x0az]\xd2\xc5\x93\xb3\x108q\xa3\x80\x11" +
"\xff=\x0b\xbd\x1e\xab\xb8n\x1c8Q\x150\xea\xbf\xd8" +
"\xa0\xd7\xe2\x17\xd7n\x01N\x1c\x0az\x81\xd0\xe9\xda\xd1" +
"\x85\xb6\xc7QhuX:\xb53\xe8\x8e\x02\xe8B\xdb" +
"\xbb\x99\xf0W\xba\x9a8\xa3\xbc\xe6\x16\xc4\xd3\x8a\xc5\xba" +
"\xa8Zt\x03\x12\x16#\x12t\xa1\x1c\xc1P\x8b\x19\xe0" +
"z[\x03I\xd6\xea\x9c\xf3G\xad\xe1\xbc\xf9\x1f1F" +
"\xf2\x95\xb4\xa6}\xfc&ih]*Kky\x94\x1b" +
"\xb9\x19+\xd1\xc8\x95\xac\xf0\xc8\x1f\xa7\xc9\xb4\xfeo\xfb" +
"\xeb\x1f\xa7\xf0\xfa}\x1e\xe5\x93!\xb7\xfe!\x09\xdf\xe1" +
"Q>\x1d\xaa\xe4~D\xbe~\x92G\xf9b\xd0\xf7~" +
"\xffq\x00\xf9\"\x8f\xc9Pe$\xfe\x9a\x06\xfe\x8a\xea" +
"\x07\xa7.B\xb7.\x8a\xe2\xd3\x00\xa9j\xaa+\x12N" +
"]\x14q\xeb\"\x11\x87\x01R\xf5$\x9f\x17\xae\x8b\x9a" +
"\xf0>\x80T#\xc9\xe7\xe3\xd4\xdb\xa6P0\x82\xca1" +
"\xab\x8f\xde\xa3j\x15\x93\xad\xd7\x88Gk\xb9\xa2f\x0b" +
"\x06\x83 \xd7\x17\x83Mo\xa8\xfcp;\xf4n3." +
"E$\xcc\xa0\xe97\xea\xae\xe1\x9e?]\xe6\xc9\xea\x85" +
"\xccHV1X&\xc5\x0c\xc1\x0d\x08\x83|T\xae\xc6" +
"\xd0\xab7@\xf0:\x19\"\xfb\xb4\x99\xac\xcf0t4" +
"J\xaa\xf4%A\x95\xee\x17\xe9t\xd9\xb8\x8bGy5" +
"\x1dm\x97{\xb4\xf2pp\xafhM+\x05\x93\x95a" +
"\x02<3\xfc^\x8f9\xa6\x17\xb2\x99$\x03\xc12&" +
"K \x9d\xb1ZO\xb1\xb8\x17\x09\xddG\x0a\xefy\x0e" +
"\xbdW\xb8\xd0#\x85\xf7R\x84\xdeSo\xf9#\x85\x87" +
"A\xd9#\x85\xfb\xc1\xe1\xfc\xd4;\xf5u4)\xdc\xb4" +
"\x18:\x94k\xea\xdd_u\xcb\xdb\xffw\x89\x92\xc8Q" +
"s\xbd\xcd /\xc1\xfd\x7f\x00\x00\x00\xff\xff\xf1\xc3d" +
"\xc6"
func init() { func init() {
schemas.Register(schema_db8274f9144abc7e, schemas.Register(schema_db8274f9144abc7e,
@ -4089,6 +4546,7 @@ func init() {
0x8635c6b4f45bf5cd, 0x8635c6b4f45bf5cd,
0x904e297b87fbecea, 0x904e297b87fbecea,
0x9496331ab9cd463f, 0x9496331ab9cd463f,
0x958096448eb3373e,
0x96b74375ce9b0ef6, 0x96b74375ce9b0ef6,
0x97b3c5c260257622, 0x97b3c5c260257622,
0x9b87b390babc2ccf, 0x9b87b390babc2ccf,
@ -4097,6 +4555,8 @@ func init() {
0xa766b24d4fe5da35, 0xa766b24d4fe5da35,
0xab6d5210c1f26687, 0xab6d5210c1f26687,
0xb046e578094b1ead, 0xb046e578094b1ead,
0xb177ca2526a3ca76,
0xb48edfbdaa25db04,
0xb4bf9861fe035d04, 0xb4bf9861fe035d04,
0xb5f39f082b9ac18a, 0xb5f39f082b9ac18a,
0xb70431c0dc014915, 0xb70431c0dc014915,
@ -4104,6 +4564,7 @@ func init() {
0xc793e50592935b4a, 0xc793e50592935b4a,
0xcbd96442ae3bb01a, 0xcbd96442ae3bb01a,
0xd4d18de97bb12de3, 0xd4d18de97bb12de3,
0xdb58ff694ba05cf9,
0xdbaa9d03d52b62dc, 0xdbaa9d03d52b62dc,
0xdc3ed6801961e502, 0xdc3ed6801961e502,
0xe3e37d096a5b564e, 0xe3e37d096a5b564e,
@ -4114,6 +4575,7 @@ func init() {
0xf2c122394f447e8e, 0xf2c122394f447e8e,
0xf2c68e2547ec3866, 0xf2c68e2547ec3866,
0xf41a0f001ad49e46, 0xf41a0f001ad49e46,
0xf548cef9dea2a4a1,
0xf5f383d2785edb86, 0xf5f383d2785edb86,
0xf71695ec7fe85497, 0xf71695ec7fe85497,
0xf9cb7f4431a307d0, 0xf9cb7f4431a307d0,

View File

@ -3,8 +3,10 @@ package websocket
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"sync"
"time" "time"
gobwas "github.com/gobwas/ws" gobwas "github.com/gobwas/ws"
@ -14,9 +16,6 @@ import (
) )
const ( const (
// Time allowed to write a message to the peer.
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the peer. // Time allowed to read the next pong message from the peer.
defaultPongWait = 60 * time.Second defaultPongWait = 60 * time.Second
@ -79,34 +78,20 @@ func (c *GorillaConn) SetDeadline(t time.Time) error {
return nil return nil
} }
// pinger simulates the websocket connection to keep it alive
func (c *GorillaConn) pinger(ctx context.Context) {
ticker := time.NewTicker(defaultPingPeriod)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if err := c.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(writeWait)); err != nil {
c.log.Debug().Msgf("failed to send ping message: %s", err)
}
case <-ctx.Done():
return
}
}
}
type Conn struct { type Conn struct {
rw io.ReadWriter rw io.ReadWriter
log *zerolog.Logger log *zerolog.Logger
// closed is a channel to indicate if Conn has been fully terminated // writeLock makes sure
shutdownC chan struct{} // 1. Only one write at a time. The pinger and Stream function can both call write.
// 2. Close only returns after in progress Write is finished, and no more Write will succeed after calling Close.
writeLock sync.Mutex
done bool
} }
func NewConn(ctx context.Context, rw io.ReadWriter, log *zerolog.Logger) *Conn { func NewConn(ctx context.Context, rw io.ReadWriter, log *zerolog.Logger) *Conn {
c := &Conn{ c := &Conn{
rw: rw, rw: rw,
log: log, log: log,
shutdownC: make(chan struct{}),
} }
go c.pinger(ctx) go c.pinger(ctx)
return c return c
@ -121,16 +106,22 @@ func (c *Conn) Read(reader []byte) (int, error) {
return copy(reader, data), nil return copy(reader, data), nil
} }
// Write will write messages to the websocket connection // Write will write messages to the websocket connection.
// It will not write to the connection after Close is called to fix TUN-5184
func (c *Conn) Write(p []byte) (int, error) { func (c *Conn) Write(p []byte) (int, error) {
c.writeLock.Lock()
defer c.writeLock.Unlock()
if c.done {
return 0, errors.New("write to closed websocket connection")
}
if err := wsutil.WriteServerBinary(c.rw, p); err != nil { if err := wsutil.WriteServerBinary(c.rw, p); err != nil {
return 0, err return 0, err
} }
return len(p), nil return len(p), nil
} }
func (c *Conn) pinger(ctx context.Context) { func (c *Conn) pinger(ctx context.Context) {
defer close(c.shutdownC)
pongMessge := wsutil.Message{ pongMessge := wsutil.Message{
OpCode: gobwas.OpPong, OpCode: gobwas.OpPong,
Payload: []byte{}, Payload: []byte{},
@ -141,7 +132,11 @@ func (c *Conn) pinger(ctx context.Context) {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
if err := wsutil.WriteServerMessage(c.rw, gobwas.OpPing, []byte{}); err != nil { done, err := c.ping()
if done {
return
}
if err != nil {
c.log.Debug().Err(err).Msgf("failed to write ping message") c.log.Debug().Err(err).Msgf("failed to write ping message")
} }
if err := wsutil.HandleClientControlMessage(c.rw, pongMessge); err != nil { if err := wsutil.HandleClientControlMessage(c.rw, pongMessge); err != nil {
@ -153,6 +148,17 @@ func (c *Conn) pinger(ctx context.Context) {
} }
} }
func (c *Conn) ping() (bool, error) {
c.writeLock.Lock()
defer c.writeLock.Unlock()
if c.done {
return true, nil
}
return false, wsutil.WriteServerMessage(c.rw, gobwas.OpPing, []byte{})
}
func (c *Conn) pingPeriod(ctx context.Context) time.Duration { func (c *Conn) pingPeriod(ctx context.Context) time.Duration {
if val := ctx.Value(PingPeriodContextKey); val != nil { if val := ctx.Value(PingPeriodContextKey); val != nil {
if period, ok := val.(time.Duration); ok { if period, ok := val.(time.Duration); ok {
@ -162,7 +168,9 @@ func (c *Conn) pingPeriod(ctx context.Context) time.Duration {
return defaultPingPeriod return defaultPingPeriod
} }
// Close waits for pinger to terminate // Close waits for the current write to finish. Further writes will return error
func (c *Conn) WaitForShutdown() { func (c *Conn) Close() {
<-c.shutdownC c.writeLock.Lock()
defer c.writeLock.Unlock()
c.done = true
} }

View File

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"sync/atomic"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -28,28 +29,64 @@ func NewResponseHeader(req *http.Request) http.Header {
return header return header
} }
type bidirectionalStreamStatus struct {
doneChan chan struct{}
anyDone uint32
}
func newBiStreamStatus() *bidirectionalStreamStatus {
return &bidirectionalStreamStatus{
doneChan: make(chan struct{}, 2),
anyDone: 0,
}
}
func (s *bidirectionalStreamStatus) markUniStreamDone() {
atomic.StoreUint32(&s.anyDone, 1)
s.doneChan <- struct{}{}
}
func (s *bidirectionalStreamStatus) waitAnyDone() {
<-s.doneChan
}
func (s *bidirectionalStreamStatus) isAnyDone() bool {
return atomic.LoadUint32(&s.anyDone) > 0
}
// Stream copies copy data to & from provided io.ReadWriters. // Stream copies copy data to & from provided io.ReadWriters.
func Stream(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) { func Stream(tunnelConn, originConn io.ReadWriter, log *zerolog.Logger) {
proxyDone := make(chan struct{}, 2) status := newBiStreamStatus()
go func() { go unidirectionalStream(tunnelConn, originConn, "origin->tunnel", status, log)
_, err := copyData(tunnelConn, originConn, "origin->tunnel") go unidirectionalStream(originConn, tunnelConn, "tunnel->origin", status, log)
if err != nil {
log.Debug().Msgf("origin to tunnel copy: %v", err)
}
proxyDone <- struct{}{}
}()
go func() {
_, err := copyData(originConn, tunnelConn, "tunnel->origin")
if err != nil {
log.Debug().Msgf("tunnel to origin copy: %v", err)
}
proxyDone <- struct{}{}
}()
// If one side is done, we are done. // If one side is done, we are done.
<-proxyDone status.waitAnyDone()
}
func unidirectionalStream(dst io.Writer, src io.Reader, dir string, status *bidirectionalStreamStatus, log *zerolog.Logger) {
defer func() {
// The bidirectional streaming spawns 2 goroutines to stream each direction.
// If any ends, the callstack returns, meaning the Tunnel request/stream (depending on http2 vs quic) will
// close. In such case, if the other direction did not stop (due to application level stopping, e.g., if a
// server/origin listens forever until closure), it may read/write from the underlying ReadWriter (backed by
// the Edge<->cloudflared transport) in an unexpected state.
if status.isAnyDone() {
// Because of this, we set this recover() logic, which kicks-in *only* if any stream is known to have
// exited. In such case, we stop a possible panic from propagating upstream.
if r := recover(); r != nil {
// We handle such unexpected errors only when we detect that one side of the streaming is done.
log.Debug().Msgf("Handled gracefully error %v in Streaming for %s", r, dir)
}
}
}()
_, err := copyData(dst, src, dir)
if err != nil {
log.Debug().Msgf("%s copy: %v", dir, err)
}
status.markUniStreamDone()
} }
// when set to true, enables logging of content copied to/from origin and tunnel // when set to true, enables logging of content copied to/from origin and tunnel