Merge branch 'cloudflare:master' into master
This commit is contained in:
commit
7713f175f3
|
@ -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.
|
|
@ -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
322
LICENSE
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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, "")
|
||||||
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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{}) {
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
|
@ -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
|
|
@ -1,7 +1,7 @@
|
||||||
//go:build !windows
|
//go:build !windows
|
||||||
// +build !windows
|
// +build !windows
|
||||||
|
|
||||||
package origin
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io/ioutil"
|
"io/ioutil"
|
|
@ -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(
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"},
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
|
@ -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)
|
||||||
}
|
}
|
|
@ -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())
|
|
@ -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}},
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package origin
|
package supervisor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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) {}
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue