mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e1a9e98cca | |||
| 057a0cc758 | |||
| ca43b0357f | |||
| 8cbd222e10 | |||
| a50c0ca9ad | |||
| 5352b3cf04 | |||
| 9552bb7bc7 | |||
| c54e8cd8e6 | |||
| aeda35699e | |||
| eee0d57ed0 | |||
| 0899d6a136 | |||
| f44e496dd9 | |||
| 3aebaaad01 | |||
| 9d9627f645 | |||
| 5c6207debc | |||
| 7220c2c214 | |||
| d17a61c15b | |||
| 5431e0ca12 | |||
| 706523389c | |||
| c2a32de35f | |||
| a1d485eca5 | |||
| 8a1ba1f8ca | |||
| b6d7076400 | |||
| 22cd8ceb8c | |||
| 9cd2780079 | |||
| 9909e9d63c | |||
| 051b2cf352 | |||
| 4cf462e322 | |||
| e56c4532ce | |||
| d78a5ba5da | |||
| 1c50618f97 | |||
| 630650b90e | |||
| d68ff390ca | |||
| 0571210374 | |||
| a16dee1d2a | |||
| 2f6f865f92 | |||
| e22422aafb | |||
| ff4cfeda0c | |||
| db01127191 | |||
| 1ff5fd3fdc | |||
| 5b12e74099 | |||
| b1edf5b96d | |||
| d07d24e5a2 | |||
| 0ab6867ae5 | |||
| ed2bac026d | |||
| e09dcf6d60 | |||
| 76fb329a65 | |||
| 7bac4b15b0 | |||
| 8a5343d0a5 | |||
| a84cbcde7e | |||
| 1a92f1acfe | |||
| c196679bc7 | |||
| 10fc450ae5 | |||
| 74556bcd7d | |||
| 97309d81ab | |||
| 0292727a95 | |||
| f33897615d | |||
| 6fa58aadba | |||
| ef3152f334 | |||
| d6036d96f0 | |||
| a6faa0c376 | |||
| 1086d5ede5 | |||
| c314d58b69 | |||
| 628545d229 | |||
| ead93e9f26 | |||
| 5f380f3a54 | |||
| 7814e870a7 | |||
| d1be558ca7 |
@@ -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.
|
||||
@@ -10,6 +10,7 @@ cscope.*
|
||||
/cloudflared.exe
|
||||
/cloudflared.msi
|
||||
/cloudflared-x86-64*
|
||||
/cloudflared.1
|
||||
/packaging
|
||||
.DS_Store
|
||||
*-session.log
|
||||
|
||||
+36
-1
@@ -1,4 +1,39 @@
|
||||
**Experimental**: This is a new format for release notes. The format and availability is subject to change.
|
||||
## 2022.3.1
|
||||
### Bug Fixes
|
||||
- Various fixes to the reliability of `quic` protocol, including an edge case that could lead to cloudflared crashing.
|
||||
|
||||
## 2022.3.0
|
||||
### New Features
|
||||
- It is now possible to configure Ingress Rules to point to an origin served by unix socket with either HTTP or HTTPS.
|
||||
If the origin starts with `unix:/` then we assume HTTP (existing behavior). Otherwise, the origin can start with
|
||||
`unix+tls:/` for HTTPS.
|
||||
|
||||
## 2022.2.1
|
||||
### New Features
|
||||
- This project now has a new LICENSE that is more compliant with open source purposes.
|
||||
|
||||
### Bug Fixes
|
||||
- Various fixes to the reliability of `quic` protocol.
|
||||
|
||||
## 2022.1.3
|
||||
### New Features
|
||||
- New `cloudflared tunnel vnet` commands to allow for private routing to be virtualized. This means that the same CIDR
|
||||
can now be used to point to two different Tunnels with `cloudflared tunnel route ip` command. More information will be
|
||||
made available on blog.cloudflare.com and developers.cloudflare.com/cloudflare-one once the feature is globally available.
|
||||
|
||||
### Bug Fixes
|
||||
- Correctly handle proxying UDP datagrams with no payload.
|
||||
- Bug fix for origins that use Server-Sent Events (SSE).
|
||||
|
||||
## 2022.1.0
|
||||
### Improvements
|
||||
- If a specific `protocol` property is defined (e.g. for `quic`), cloudflared no longer falls back to an older protocol
|
||||
(such as `http2`) in face of connectivity errors. This is important because some features are only supported in a specific
|
||||
protocol (e.g. UDP proxying only works for `quic`). Hence, if a user chooses a protocol, cloudflared now adheres to it
|
||||
no matter what.
|
||||
|
||||
### Bug Fixes
|
||||
- Stopping cloudflared running with `quic` protocol now respects graceful shutdown.
|
||||
|
||||
## 2021.12.2
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,155 +1,202 @@
|
||||
SERVICES AGREEMENT
|
||||
|
||||
Your installation of this software is symbol of your signature indicating that
|
||||
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.
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
1. GRANT OF RIGHTS
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1.1 Grant of License. The Solution is licensed by CloudFlare and its
|
||||
licensors, not sold. Subject to the terms and conditions of this Agreement,
|
||||
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. Definitions.
|
||||
|
||||
1.2 Restrictions. The license granted herein is granted solely to you and
|
||||
not, by implication or otherwise, to any of your parents, subsidiaries or
|
||||
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.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
1.3 Ownership. As between the parties, CloudFlare and/or its licensors own
|
||||
and shall retain all right, title, and interest in and to the Solution,
|
||||
including any and all technology embodied therein, including all copyrights,
|
||||
patents, trade secrets, trade dress and other proprietary rights associated
|
||||
therewith, and any derivative works created there from.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
2. LIMITATION OF LIABILITY
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
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.
|
||||
|
||||
YOU EXPRESSLY ACKNOWLEDGE AND AGREE THAT DOWNLOADING THE SOFTWARE IS AT YOUR
|
||||
SOLE RISK. THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTY OF ANY KIND
|
||||
AND CLOUDFLARE, ITS LICENSORS AND ITS AUTHORIZED REPRESENTATIVES (TOGETHER FOR
|
||||
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.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
3. CONFIDENTIALITY
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
It may be necessary during the set up and performance of the Solution for the
|
||||
parties to exchange Confidential Information. "Confidential Information" means
|
||||
any information whether oral, or written, of a private, secret, proprietary or
|
||||
confidential nature, concerning either party or its business operations,
|
||||
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.
|
||||
"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.
|
||||
|
||||
4. TERM AND TERMINATION
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
4.1 Term. This Agreement shall be effective upon download or install of the
|
||||
Software.
|
||||
"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.2 Termination. This Agreement may be terminated by CloudFlare or its
|
||||
authorized representative by written notice to you if any of the following
|
||||
events occur: (i) you fail to pay any amounts due for the Services and the
|
||||
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.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
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.3 Effect of Termination. Upon the termination of this Agreement for any
|
||||
reason: (1) all license rights granted hereunder shall terminate and (2) all
|
||||
Confidential Information shall be returned to the disclosing party or destroyed.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
5. MISCELLANEOUS
|
||||
2. Grant of Copyright 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
|
||||
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.1 Assignment. You may not assign any of your rights or delegate any of
|
||||
your obligations under this Agreement, whether by operation of law or otherwise,
|
||||
without the prior express written consent of CloudFlare or its authorized
|
||||
representative. Any such assignment without the prior express written consent
|
||||
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.
|
||||
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.2 Waiver and Amendment. No modification, amendment or waiver of any
|
||||
provision of this Agreement shall be effective unless in writing and signed by
|
||||
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.
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
5.3 Governing Law. This Agreement shall be governed by the laws of the State
|
||||
of California, USA, excluding conflict of laws and provisions, and excluding the
|
||||
United Nations Convention on Contracts for the International Sale of Goods.
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
5.4 Notices. All notices, demands or consents required or permitted under
|
||||
this Agreement shall be in writing. Notice shall be sent to you at the e-mail
|
||||
address provided by you to CloudFlare or its authorized representative in
|
||||
connection with the Solution.
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
5.5 Independent Contractors. The parties are independent contractors.
|
||||
Neither party shall be deemed to be an employee, agent, partner or legal
|
||||
representative of the other for any purpose and neither shall have any right,
|
||||
power or authority to create any obligation or responsibility on behalf of the
|
||||
other.
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
5.6 Severability. If any provision of this Agreement is held by a court of
|
||||
competent jurisdiction to be contrary to law, such provision shall be changed
|
||||
and interpreted so as to best accomplish the objectives of the original
|
||||
provision to the fullest extent allowed by law and the remaining provisions of
|
||||
this Agreement shall remain in full force and effect.
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
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.7 Force Majeure. CloudFlare shall not be liable to the other party for any
|
||||
failure or delay in performance caused by reasons beyond its reasonable control.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5.8 Complete Understanding. This Agreement and the Related Agreement
|
||||
constitute the final, complete and exclusive agreement between the parties with
|
||||
respect to the subject matter hereof, and supersedes all previous written and
|
||||
oral agreements and communications related to the subject matter of this
|
||||
Agreement. To the extent this Agreement and the Related Agreement conflict,
|
||||
this Agreement shall control.
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
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.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
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.
|
||||
@@ -3,15 +3,19 @@ MSI_VERSION := $(shell git tag -l --sort=v:refname | grep "w" | tail -1 | cut
|
||||
#MSI_VERSION expects the format of the tag to be: (wX.X.X). Starts with the w character to not break cfsetup.
|
||||
#e.g. w3.0.1 or w4.2.10. It trims off the w character when creating the MSI.
|
||||
|
||||
ifeq ($(FIPS), true)
|
||||
ifeq ($(ORIGINAL_NAME), true)
|
||||
# Used for builds that want FIPS compilation but want the artifacts generated to still have the original name.
|
||||
BINARY_NAME := cloudflared
|
||||
else ifeq ($(FIPS), true)
|
||||
# Used for FIPS compliant builds that do not match the case above.
|
||||
BINARY_NAME := cloudflared-fips
|
||||
else
|
||||
# Used for all other (non-FIPS) builds.
|
||||
BINARY_NAME := cloudflared
|
||||
endif
|
||||
|
||||
ifeq ($(NIGHTLY), true)
|
||||
# We do not release FIPS in NIGHTLY, so no need to consider that case here.
|
||||
DEB_PACKAGE_NAME := cloudflared-nightly
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)-nightly
|
||||
NIGHTLY_FLAGS := --conflicts cloudflared --replaces cloudflared
|
||||
else
|
||||
DEB_PACKAGE_NAME := $(BINARY_NAME)
|
||||
@@ -19,6 +23,9 @@ endif
|
||||
|
||||
DATE := $(shell date -u '+%Y-%m-%d-%H%M UTC')
|
||||
VERSION_FLAGS := -X "main.Version=$(VERSION)" -X "main.BuildTime=$(DATE)"
|
||||
ifdef PACKAGE_MANAGER
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "github.com/cloudflare/cloudflared/cmd/cloudflared/updater.BuiltForPackageManager=$(PACKAGE_MANAGER)"
|
||||
endif
|
||||
|
||||
LINK_FLAGS :=
|
||||
ifeq ($(FIPS), true)
|
||||
@@ -33,10 +40,11 @@ ifneq ($(GO_BUILD_TAGS),)
|
||||
GO_BUILD_TAGS := -tags "$(GO_BUILD_TAGS)"
|
||||
endif
|
||||
|
||||
IMPORT_PATH := github.com/cloudflare/cloudflared
|
||||
PACKAGE_DIR := $(CURDIR)/packaging
|
||||
INSTALL_BINDIR := /usr/bin/
|
||||
MAN_DIR := /usr/share/man/man1/
|
||||
IMPORT_PATH := github.com/cloudflare/cloudflared
|
||||
PACKAGE_DIR := $(CURDIR)/packaging
|
||||
PREFIX := /usr
|
||||
INSTALL_BINDIR := $(PREFIX)/bin/
|
||||
INSTALL_MANDIR := $(PREFIX)/share/man/man1/
|
||||
|
||||
LOCAL_ARCH ?= $(shell uname -m)
|
||||
ifneq ($(GOARCH),)
|
||||
@@ -137,36 +145,40 @@ publish-deb: cloudflared-deb
|
||||
publish-rpm: cloudflared-rpm
|
||||
$(call publish_package,rpm,yum)
|
||||
|
||||
cloudflared.1: cloudflared_man_template
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > cloudflared.1
|
||||
|
||||
install: cloudflared cloudflared.1
|
||||
mkdir -p $(DESTDIR)$(INSTALL_BINDIR) $(DESTDIR)$(INSTALL_MANDIR)
|
||||
install -m755 cloudflared $(DESTDIR)$(INSTALL_BINDIR)/cloudflared
|
||||
install -m644 cloudflared.1 $(DESTDIR)$(INSTALL_MANDIR)/cloudflared.1
|
||||
|
||||
# When we build packages, the package name will be FIPS-aware.
|
||||
# But we keep the binary installed by it to be named "cloudflared" regardless.
|
||||
define build_package
|
||||
mkdir -p $(PACKAGE_DIR)
|
||||
cp cloudflared $(PACKAGE_DIR)/cloudflared
|
||||
cat cloudflared_man_template | sed -e 's/\$${VERSION}/$(VERSION)/; s/\$${DATE}/$(DATE)/' > $(PACKAGE_DIR)/cloudflared.1
|
||||
cp cloudflared.1 $(PACKAGE_DIR)/cloudflared.1
|
||||
fakeroot fpm -C $(PACKAGE_DIR) -s dir -t $(1) \
|
||||
--description 'Cloudflare Tunnel daemon' \
|
||||
--vendor 'Cloudflare' \
|
||||
--license 'Cloudflare Service Agreement' \
|
||||
--license 'Apache License Version 2.0' \
|
||||
--url 'https://github.com/cloudflare/cloudflared' \
|
||||
-m 'Cloudflare <support@cloudflare.com>' \
|
||||
-a $(TARGET_ARCH) -v $(VERSION) -n $(DEB_PACKAGE_NAME) $(NIGHTLY_FLAGS) --after-install postinst.sh --after-remove postrm.sh \
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(MAN_DIR)
|
||||
cloudflared=$(INSTALL_BINDIR) cloudflared.1=$(INSTALL_MANDIR)
|
||||
endef
|
||||
|
||||
.PHONY: cloudflared-deb
|
||||
cloudflared-deb: cloudflared
|
||||
cloudflared-deb: cloudflared cloudflared.1
|
||||
$(call build_package,deb)
|
||||
|
||||
.PHONY: cloudflared-internal-deb
|
||||
cloudflared-internal-deb: cloudflared-deb
|
||||
bash -c 'for f in cloudflared-fips_*.deb; do mv -- "$$f" "$${f/-fips/}"; done'
|
||||
|
||||
.PHONY: cloudflared-rpm
|
||||
cloudflared-rpm: cloudflared
|
||||
cloudflared-rpm: cloudflared cloudflared.1
|
||||
$(call build_package,rpm)
|
||||
|
||||
.PHONY: cloudflared-pkg
|
||||
cloudflared-pkg: cloudflared
|
||||
cloudflared-pkg: cloudflared cloudflared.1
|
||||
$(call build_package,osxpkg)
|
||||
|
||||
.PHONY: cloudflared-msi
|
||||
|
||||
@@ -1,3 +1,79 @@
|
||||
2022.3.2
|
||||
- 2022-03-10 TUN-5833: Create constant for allow-remote-config
|
||||
- 2022-03-15 TUN-5867: Return error if service was already installed
|
||||
- 2022-03-16 TUN-5833: Send feature `allow_remote_config` if Tunnel is run with --token
|
||||
- 2022-03-08 TUN-5849: Remove configuration debug log
|
||||
- 2022-03-08 TUN-5850: Update CHANGES.md with latest releases
|
||||
- 2022-03-08 TUN-5851: Update all references to point to Apache License 2.0
|
||||
- 2022-03-07 TUN-5853 Add "install" make target and build package manager info into executable
|
||||
- 2022-03-08 TUN-5801: Add custom wrapper for OriginConfig for JSON serde
|
||||
- 2022-03-09 TUN-5703: Add prometheus metric for current configuration version
|
||||
- 2022-02-05 CC-796: Remove dependency on unsupported version of go-oidc
|
||||
|
||||
2022.3.1
|
||||
- 2022-03-04 TUN-5837: Log panic recovery in http2 logic with debug level log
|
||||
- 2022-03-04 TUN-5696: HTTP/2 Configuration Update
|
||||
- 2022-03-04 TUN-5836: Avoid websocket#Stream function from crashing cloudflared with unexpected memory access
|
||||
- 2022-03-05 TUN-5836: QUIC transport no longer sets body to nil in any condition
|
||||
|
||||
2022.3.0
|
||||
- 2022-03-02 TUN-5680: Adapt component tests for new service install based on token
|
||||
- 2022-02-21 TUN-5682: Remove name field from credentials
|
||||
- 2022-02-21 TUN-5681: Add support for running tunnel using Token
|
||||
- 2022-02-28 TUN-5824: Update updater no-update-in-shell link
|
||||
- 2022-02-28 TUN-5823: Warn about legacy flags that are ignored when ingress rules are used
|
||||
- 2022-02-28 TUN-5737: Support https protocol over unix socket origin
|
||||
- 2022-02-23 TUN-5679: Add support for service install using Tunnel Token
|
||||
|
||||
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-01-21 TUN-5477: Unhide vnet commands
|
||||
- 2022-01-24 TUN-5669: Change network command to vnet
|
||||
- 2022-01-25 TUN-5675: Remove github.com/dgrijalva/jwt-go dependency by upgrading coredns version
|
||||
- 2022-01-27 TUN-5719: Re-attempt connection to edge with QUIC despite network error when there is no fallback
|
||||
- 2022-01-28 TUN-5724: Fix SSE streaming by guaranteeing we write everything we read
|
||||
- 2022-01-17 TUN-5547: Bump golang x/net package to fix http2 transport bugs
|
||||
- 2022-01-19 TUN-5659: Proxy UDP with zero-byte payload
|
||||
- 2021-10-22 Add X-Forwarded-Host for http proxy
|
||||
|
||||
2022.1.2
|
||||
- 2022-01-13 TUN-5650: Fix pynacl version to 1.4.0 and pygithub version to 1.55 so release doesn't break unexpectedly
|
||||
|
||||
2022.1.1
|
||||
- 2022-01-10 TUN-5631: Build everything with go 1.17.5
|
||||
- 2022-01-06 TUN-5623: Configure quic max datagram frame size to 1350 bytes for none Windows platforms
|
||||
|
||||
2022.1.0
|
||||
- 2022-01-03 TUN-5612: Add support for specifying TLS min/max version
|
||||
- 2022-01-03 TUN-5612: Make tls min/max version public visible
|
||||
- 2022-01-03 TUN-5551: Internally published debian artifacts are now named just cloudflared even though they are FIPS compliant
|
||||
- 2022-01-04 TUN-5600: Close QUIC transports as soon as possible while respecting graceful shutdown
|
||||
- 2022-01-05 TUN-5616: Never fallback transport if user chooses it on purpose
|
||||
- 2022-01-05 TUN-5204: Unregister QUIC transports on disconnect
|
||||
- 2022-01-04 TUN-5600: Add coverage to component tests for various transports
|
||||
|
||||
2021.12.4
|
||||
- 2021-12-27 TUN-5482: Refactor tunnelstore client related packages for more coherent package
|
||||
- 2021-12-27 TUN-5551: Change internally published debian package to be FIPS compliant
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, lo
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
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 {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import (
|
||||
)
|
||||
|
||||
type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, error)
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
Usage: "Show only routes with this comment.",
|
||||
}
|
||||
filterIpRouteByVnet = cli.StringFlag{
|
||||
Name: "filter-virtual-network-id",
|
||||
Name: "filter-vnet-id",
|
||||
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||
}
|
||||
|
||||
|
||||
+11
-2
@@ -23,6 +23,11 @@ type Tunnel struct {
|
||||
Connections []Connection `json:"connections"`
|
||||
}
|
||||
|
||||
type TunnelWithToken struct {
|
||||
Tunnel
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ColoName string `json:"colo_name"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
@@ -63,7 +68,7 @@ func (cp CleanupParams) encode() string {
|
||||
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 == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
@@ -83,7 +88,11 @@ func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*Tunnel, er
|
||||
|
||||
switch resp.StatusCode {
|
||||
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:
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
+13
-22
@@ -1,5 +1,5 @@
|
||||
pinned_go: &pinned_go go=1.17-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.16.6-7
|
||||
pinned_go: &pinned_go go=1.17.5-1
|
||||
pinned_go_fips: &pinned_go_fips go-boring=1.17.5-1
|
||||
|
||||
build_dir: &build_dir /cfsetup_build
|
||||
default-flavor: buster
|
||||
@@ -43,7 +43,8 @@ stretch: &stretch
|
||||
pre-cache: &github_release_pkgs_pre_cache
|
||||
- wget https://github.com/sudarshan-reddy/msitools/releases/download/v0.101b/wixl -P /usr/local/bin
|
||||
- chmod a+x /usr/local/bin/wixl
|
||||
- pip3 install pygithub
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
# build all packages (except macos and FIPS) and move them to /cfsetup/built_artifacts
|
||||
- ./build-packages.sh
|
||||
@@ -93,7 +94,8 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- make cloudflared-internal-deb
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-fips-internal-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_fips_deb_deps
|
||||
@@ -102,7 +104,8 @@ stretch: &stretch
|
||||
- export GOARCH=amd64
|
||||
- export NIGHTLY=true
|
||||
- export FIPS=true
|
||||
- make cloudflared-internal-deb
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
@@ -132,7 +135,8 @@ stretch: &stretch
|
||||
- python3-setuptools
|
||||
- python3-pip
|
||||
pre-cache: &install_pygithub
|
||||
- pip3 install pygithub
|
||||
- pip3 install pynacl==1.4.0
|
||||
- pip3 install pygithub==1.55
|
||||
post-cache:
|
||||
- make github-mac-upload
|
||||
test:
|
||||
@@ -242,23 +246,10 @@ centos-7:
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.3.linux-amd64.tar.gz
|
||||
- wget https://go.dev/dl/go1.17.5.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.17.5.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make publish-rpm
|
||||
build-rpm:
|
||||
build_dir: *build_dir
|
||||
builddeps: *el7_builddeps
|
||||
pre-cache:
|
||||
- yum install -y fakeroot
|
||||
- yum upgrade -y binutils-2.27-44.base.el7.x86_64
|
||||
- wget https://golang.org/dl/go1.16.3.linux-amd64.tar.gz -P /tmp/
|
||||
- tar -C /usr/local -xzf /tmp/go1.16.3.linux-amd64.tar.gz
|
||||
post-cache:
|
||||
- export PATH=$PATH:/usr/local/go/bin
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-rpm
|
||||
- make publish-rpm
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/tunnel"
|
||||
)
|
||||
|
||||
func buildArgsForToken(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
token := c.Args().First()
|
||||
if _, err := tunnel.ParseToken(token); err != nil {
|
||||
return nil, cliutil.UsageError("Provided tunnel token is not valid (%s).", err)
|
||||
}
|
||||
|
||||
return []string{
|
||||
"tunnel", "run", "--token", token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getServiceExtraArgsFromCliArgs(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
if c.NArg() > 0 {
|
||||
// currently, we only support extra args for token
|
||||
return buildArgsForToken(c, log)
|
||||
} else {
|
||||
// empty extra args
|
||||
return make([]string, 0), nil
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -26,12 +25,6 @@ func runApp(app *cli.App, graceShutdownC chan struct{}) {
|
||||
Name: "install",
|
||||
Usage: "Install Cloudflare Tunnel as a system service",
|
||||
Action: cliutil.ConfiguredAction(installLinuxService),
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "legacy",
|
||||
Usage: "Generate service file for non-named tunnels",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "uninstall",
|
||||
@@ -62,7 +55,7 @@ After=network.target
|
||||
[Service]
|
||||
TimeoutStartSec=0
|
||||
Type=notify
|
||||
ExecStart={{ .Path }} --config /etc/cloudflared/config.yml --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
|
||||
ExecStart={{ .Path }} --no-autoupdate{{ range .ExtraArgs }} {{ . }}{{ end }}
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
@@ -112,7 +105,7 @@ var sysvTemplate = ServiceTemplate{
|
||||
# Description: Cloudflare Tunnel agent
|
||||
### END INIT INFO
|
||||
name=$(basename $(readlink -f $0))
|
||||
cmd="{{.Path}} --config /etc/cloudflared/config.yml --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
cmd="{{.Path}} --pidfile /var/run/$name.pid --autoupdate-freq 24h0m0s{{ range .ExtraArgs }} {{ . }}{{ end }}"
|
||||
pid_file="/var/run/$name.pid"
|
||||
stdout_log="/var/log/$name.log"
|
||||
stderr_log="/var/log/$name.err"
|
||||
@@ -191,27 +184,6 @@ func isSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile string, log *zerolog.Logger) error {
|
||||
srcCredentialPath := filepath.Join(userConfigDir, userCredentialFile)
|
||||
destCredentialPath := filepath.Join(serviceConfigDir, serviceCredentialFile)
|
||||
if srcCredentialPath != destCredentialPath {
|
||||
if err := copyCredential(srcCredentialPath, destCredentialPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
srcConfigPath := filepath.Join(userConfigDir, userConfigFile)
|
||||
destConfigPath := filepath.Join(serviceConfigDir, serviceConfigFile)
|
||||
if srcConfigPath != destConfigPath {
|
||||
if err := copyConfig(srcConfigPath, destConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Info().Msgf("Copied %s to %s", srcConfigPath, destConfigPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func installLinuxService(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
@@ -223,52 +195,19 @@ func installLinuxService(c *cli.Context) error {
|
||||
Path: etPath,
|
||||
}
|
||||
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
var extraArgsFunc func(c *cli.Context, log *zerolog.Logger) ([]string, error)
|
||||
if c.NArg() == 0 {
|
||||
extraArgsFunc = buildArgsForConfig
|
||||
} else {
|
||||
extraArgsFunc = buildArgsForToken
|
||||
}
|
||||
|
||||
extraArgs, err := extraArgsFunc(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("legacy") {
|
||||
userConfigDir := filepath.Dir(c.String("config"))
|
||||
userConfigFile := filepath.Base(c.String("config"))
|
||||
userCredentialFile := config.DefaultCredentialFile
|
||||
if err = copyUserConfiguration(userConfigDir, userConfigFile, userCredentialFile, log); err != nil {
|
||||
log.Err(err).Msgf("Failed to copy user configuration. Before running the service, ensure that %s contains two files, %s and %s",
|
||||
serviceConfigDir, serviceCredentialFile, serviceConfigFile)
|
||||
return err
|
||||
}
|
||||
templateArgs.ExtraArgs = []string{
|
||||
"--origincert", serviceConfigDir + "/" + serviceCredentialFile,
|
||||
}
|
||||
} else {
|
||||
src, _, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// can't use context because this command doesn't define "credentials-file" flag
|
||||
configPresent := func(s string) bool {
|
||||
val, err := src.String(s)
|
||||
return err == nil && val != ""
|
||||
}
|
||||
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
|
||||
return fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
|
||||
tunnel: TUNNEL-UUID
|
||||
credentials-file: CREDENTIALS-FILE
|
||||
`, src.Source())
|
||||
}
|
||||
if src.Source() != serviceConfigPath {
|
||||
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
|
||||
return fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
}
|
||||
|
||||
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
|
||||
return fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
templateArgs.ExtraArgs = []string{
|
||||
"tunnel", "run",
|
||||
}
|
||||
}
|
||||
templateArgs.ExtraArgs = extraArgs
|
||||
|
||||
switch {
|
||||
case isSystemd():
|
||||
@@ -280,6 +219,42 @@ credentials-file: CREDENTIALS-FILE
|
||||
}
|
||||
}
|
||||
|
||||
func buildArgsForConfig(c *cli.Context, log *zerolog.Logger) ([]string, error) {
|
||||
if err := ensureConfigDirExists(serviceConfigDir); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
src, _, err := config.ReadConfigFile(c, log)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// can't use context because this command doesn't define "credentials-file" flag
|
||||
configPresent := func(s string) bool {
|
||||
val, err := src.String(s)
|
||||
return err == nil && val != ""
|
||||
}
|
||||
if src.TunnelID == "" || !configPresent(tunnel.CredFileFlag) {
|
||||
return nil, fmt.Errorf(`Configuration file %s must contain entries for the tunnel to run and its associated credentials:
|
||||
tunnel: TUNNEL-UUID
|
||||
credentials-file: CREDENTIALS-FILE
|
||||
`, src.Source())
|
||||
}
|
||||
if src.Source() != serviceConfigPath {
|
||||
if exists, err := config.FileExists(serviceConfigPath); err != nil || exists {
|
||||
return nil, fmt.Errorf("Possible conflicting configuration in %[1]s and %[2]s. Either remove %[2]s or run `cloudflared --config %[2]s service install`", src.Source(), serviceConfigPath)
|
||||
}
|
||||
|
||||
if err := copyFile(src.Source(), serviceConfigPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy %s to %s: %w", src.Source(), serviceConfigPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return []string{
|
||||
"--config", "/etc/cloudflared/config.yml", "tunnel", "run",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func installSystemd(templateArgs *ServiceTemplateArgs, log *zerolog.Logger) error {
|
||||
for _, serviceTemplate := range systemdTemplates {
|
||||
err := serviceTemplate.Generate(templateArgs)
|
||||
|
||||
@@ -50,6 +50,9 @@ func newLaunchdTemplate(installPath, stdoutPath, stderrPath string) *ServiceTemp
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ .Path }}</string>
|
||||
{{- range $i, $item := .ExtraArgs}}
|
||||
<string>{{ $item }}</string>
|
||||
{{- end}}
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
@@ -129,6 +132,13 @@ func installLaunchd(c *cli.Context) error {
|
||||
log.Err(err).Msg("Error determining install path")
|
||||
return errors.Wrap(err, "Error determining install path")
|
||||
}
|
||||
extraArgs, err := getServiceExtraArgsFromCliArgs(c, log)
|
||||
if err != nil {
|
||||
errMsg := "Unable to determine extra arguments for launch daemon"
|
||||
log.Err(err).Msg(errMsg)
|
||||
return errors.Wrap(err, errMsg)
|
||||
}
|
||||
|
||||
stdoutPath, err := stdoutPath()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error determining stdout path")
|
||||
@@ -140,7 +150,7 @@ func installLaunchd(c *cli.Context) error {
|
||||
return errors.Wrap(err, "error determining stderr path")
|
||||
}
|
||||
launchdTemplate := newLaunchdTemplate(installPath, stdoutPath, stderrPath)
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath}
|
||||
templateArgs := ServiceTemplateArgs{Path: etPath, ExtraArgs: extraArgs}
|
||||
err = launchdTemplate.Generate(&templateArgs)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("error generating launchd template")
|
||||
|
||||
@@ -69,7 +69,7 @@ func main() {
|
||||
app.Copyright = fmt.Sprintf(
|
||||
`(c) %d Cloudflare Inc.
|
||||
Your installation of cloudflared software constitutes a symbol of your signature indicating that you accept
|
||||
the terms of the Cloudflare License (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/license),
|
||||
the terms of the Apache License Version 2.0 (https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/license),
|
||||
Terms (https://www.cloudflare.com/terms/) and Privacy Policy (https://www.cloudflare.com/privacypolicy/).`,
|
||||
time.Now().Year(),
|
||||
)
|
||||
|
||||
@@ -43,6 +43,10 @@ func (st *ServiceTemplate) Generate(args *ServiceTemplateArgs) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = os.Stat(resolvedPath); err == nil {
|
||||
return fmt.Errorf("cloudflared service is already installed at %s", resolvedPath)
|
||||
}
|
||||
|
||||
var buffer bytes.Buffer
|
||||
err = tmpl.Execute(&buffer, args)
|
||||
if err != nil {
|
||||
|
||||
@@ -31,8 +31,9 @@ import (
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/metrics"
|
||||
"github.com/cloudflare/cloudflared/origin"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
"github.com/cloudflare/cloudflared/supervisor"
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
"github.com/cloudflare/cloudflared/tunneldns"
|
||||
)
|
||||
@@ -101,8 +102,7 @@ func Commands() []*cli.Command {
|
||||
buildLoginSubcommand(false),
|
||||
buildCreateCommand(),
|
||||
buildRouteCommand(),
|
||||
// TODO TUN-5477 this should not be hidden
|
||||
buildVirtualNetworkSubcommand(true),
|
||||
buildVirtualNetworkSubcommand(false),
|
||||
buildRunCommand(),
|
||||
buildListCommand(),
|
||||
buildInfoCommand(),
|
||||
@@ -224,7 +224,7 @@ func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
|
||||
func StartServer(
|
||||
c *cli.Context,
|
||||
info *cliutil.BuildInfo,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
log *zerolog.Logger,
|
||||
isUIEnabled bool,
|
||||
) error {
|
||||
@@ -334,7 +334,7 @@ func StartServer(
|
||||
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 {
|
||||
log.Err(err).Msg("Couldn't start tunnel")
|
||||
return err
|
||||
@@ -354,11 +354,12 @@ func StartServer(
|
||||
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
|
||||
}
|
||||
|
||||
reconnectCh := make(chan origin.ReconnectSignal, 1)
|
||||
reconnectCh := make(chan supervisor.ReconnectSignal, 1)
|
||||
if c.IsSet("stdin-control") {
|
||||
log.Info().Msg("Enabling control through stdin")
|
||||
go stdinControl(reconnectCh, log)
|
||||
@@ -370,7 +371,7 @@ func StartServer(
|
||||
wg.Done()
|
||||
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 {
|
||||
@@ -378,7 +379,7 @@ func StartServer(
|
||||
info.Version(),
|
||||
hostname,
|
||||
metricsListener.Addr().String(),
|
||||
&ingressRules,
|
||||
dynamicConfig.Ingress,
|
||||
tunnelConfig.HAConnections,
|
||||
)
|
||||
app := tunnelUI.Launch(ctx, log, logTransport)
|
||||
@@ -723,43 +724,43 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: ingress.Socks5Flag,
|
||||
Usage: "specify if this tunnel is running as a SOCK5 Server",
|
||||
Usage: legacyTunnelFlag("specify if this tunnel is running as a SOCK5 Server"),
|
||||
EnvVars: []string{"TUNNEL_SOCKS"},
|
||||
Value: false,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: ingress.ProxyConnectTimeoutFlag,
|
||||
Usage: "HTTP proxy timeout for establishing a new connection",
|
||||
Usage: legacyTunnelFlag("HTTP proxy timeout for establishing a new connection"),
|
||||
Value: time.Second * 30,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: ingress.ProxyTLSTimeoutFlag,
|
||||
Usage: "HTTP proxy timeout for completing a TLS handshake",
|
||||
Usage: legacyTunnelFlag("HTTP proxy timeout for completing a TLS handshake"),
|
||||
Value: time.Second * 10,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: ingress.ProxyTCPKeepAliveFlag,
|
||||
Usage: "HTTP proxy TCP keepalive duration",
|
||||
Usage: legacyTunnelFlag("HTTP proxy TCP keepalive duration"),
|
||||
Value: time.Second * 30,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: ingress.ProxyNoHappyEyeballsFlag,
|
||||
Usage: "HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback",
|
||||
Usage: legacyTunnelFlag("HTTP proxy should disable \"happy eyeballs\" for IPv4/v6 fallback"),
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewIntFlag(&cli.IntFlag{
|
||||
Name: ingress.ProxyKeepAliveConnectionsFlag,
|
||||
Usage: "HTTP proxy maximum keepalive connection pool size",
|
||||
Usage: legacyTunnelFlag("HTTP proxy maximum keepalive connection pool size"),
|
||||
Value: 100,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewDurationFlag(&cli.DurationFlag{
|
||||
Name: ingress.ProxyKeepAliveTimeoutFlag,
|
||||
Usage: "HTTP proxy timeout for closing an idle connection",
|
||||
Usage: legacyTunnelFlag("HTTP proxy timeout for closing an idle connection"),
|
||||
Value: time.Second * 90,
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
@@ -777,13 +778,13 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: ingress.HTTPHostHeaderFlag,
|
||||
Usage: "Sets the HTTP Host header for the local webserver.",
|
||||
Usage: legacyTunnelFlag("Sets the HTTP Host header for the local webserver."),
|
||||
EnvVars: []string{"TUNNEL_HTTP_HOST_HEADER"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: ingress.OriginServerNameFlag,
|
||||
Usage: "Hostname on the origin server certificate.",
|
||||
Usage: legacyTunnelFlag("Hostname on the origin server certificate."),
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_SERVER_NAME"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
@@ -795,19 +796,19 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
}),
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
Name: tlsconfig.OriginCAPoolFlag,
|
||||
Usage: "Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.",
|
||||
Usage: legacyTunnelFlag("Path to the CA for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare."),
|
||||
EnvVars: []string{"TUNNEL_ORIGIN_CA_POOL"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: ingress.NoTLSVerifyFlag,
|
||||
Usage: "Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted.",
|
||||
Usage: legacyTunnelFlag("Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted. Note: The connection from your machine to Cloudflare's Edge is still encrypted."),
|
||||
EnvVars: []string{"NO_TLS_VERIFY"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
altsrc.NewBoolFlag(&cli.BoolFlag{
|
||||
Name: ingress.NoChunkedEncodingFlag,
|
||||
Usage: "Disables chunked transfer encoding; useful if you are running a WSGI server.",
|
||||
Usage: legacyTunnelFlag("Disables chunked transfer encoding; useful if you are running a WSGI server."),
|
||||
EnvVars: []string{"TUNNEL_NO_CHUNKED_ENCODING"},
|
||||
Hidden: shouldHide,
|
||||
}),
|
||||
@@ -815,6 +816,15 @@ func configureProxyFlags(shouldHide bool) []cli.Flag {
|
||||
return append(flags, sshFlags(shouldHide)...)
|
||||
}
|
||||
|
||||
func legacyTunnelFlag(msg string) string {
|
||||
return fmt.Sprintf(
|
||||
"%s This flag only takes effect if you define your origin with `--url` and if you do not use ingress rules."+
|
||||
" The recommended way is to rely on ingress rules and define this property under `originRequest` as per"+
|
||||
" https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/configuration-file/ingress",
|
||||
msg,
|
||||
)
|
||||
}
|
||||
|
||||
func sshFlags(shouldHide bool) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
altsrc.NewStringFlag(&cli.StringFlag{
|
||||
@@ -999,7 +1009,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 {
|
||||
scanner := bufio.NewScanner(os.Stdin)
|
||||
for scanner.Scan() {
|
||||
@@ -1010,7 +1020,7 @@ func stdinControl(reconnectCh chan origin.ReconnectSignal, log *zerolog.Logger)
|
||||
case "":
|
||||
break
|
||||
case "reconnect":
|
||||
var reconnect origin.ReconnectSignal
|
||||
var reconnect supervisor.ReconnectSignal
|
||||
if len(parts) > 1 {
|
||||
var err error
|
||||
if reconnect.Delay, err = time.ParseDuration(parts[1]); err != nil {
|
||||
|
||||
@@ -23,7 +23,8 @@ import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"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"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"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)
|
||||
}
|
||||
|
||||
@@ -152,44 +153,48 @@ func prepareTunnelConfig(
|
||||
info *cliutil.BuildInfo,
|
||||
log, logTransport *zerolog.Logger,
|
||||
observer *connection.Observer,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
) (*origin.TunnelConfig, ingress.Ingress, error) {
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
) (*supervisor.TunnelConfig, *orchestration.Config, error) {
|
||||
isNamedTunnel := namedTunnel != nil
|
||||
|
||||
configHostname := c.String("hostname")
|
||||
hostname, err := validation.ValidateHostname(configHostname)
|
||||
if err != nil {
|
||||
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")
|
||||
if !c.IsSet("id") {
|
||||
clientID, err = generateRandomClientID(log)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := NewTagSliceFromCLI(c.StringSlice("tag"))
|
||||
if err != nil {
|
||||
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})
|
||||
|
||||
var (
|
||||
ingressRules ingress.Ingress
|
||||
classicTunnel *connection.ClassicTunnelConfig
|
||||
classicTunnel *connection.ClassicTunnelProperties
|
||||
)
|
||||
cfg := config.GetConfiguration()
|
||||
if isNamedTunnel {
|
||||
clientUUID, err := uuid.NewRandom()
|
||||
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)
|
||||
features := append(c.StringSlice("features"), origin.FeatureSerializedHeaders)
|
||||
features := append(c.StringSlice("features"), supervisor.FeatureSerializedHeaders)
|
||||
if c.IsSet(TunnelTokenFlag) {
|
||||
features = append(features, supervisor.FeatureAllowRemoteConfig)
|
||||
log.Info().Msg("Will be fetching remotely managed configuration from Cloudflare API")
|
||||
}
|
||||
namedTunnel.Client = tunnelpogs.ClientInfo{
|
||||
ClientID: clientUUID[:],
|
||||
Features: dedup(features),
|
||||
@@ -198,10 +203,10 @@ func prepareTunnelConfig(
|
||||
}
|
||||
ingressRules, err = ingress.ParseIngress(cfg)
|
||||
if err != nil && err != ingress.ErrNoIngressRules {
|
||||
return nil, ingress.Ingress{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
if !ingressRules.IsEmpty() && c.IsSet("url") {
|
||||
return nil, ingress.Ingress{}, ingress.ErrURLIncompatibleWithIngress
|
||||
return nil, nil, ingress.ErrURLIncompatibleWithIngress
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -212,10 +217,10 @@ func prepareTunnelConfig(
|
||||
|
||||
originCert, err := getOriginCert(originCertPath, &originCertLog)
|
||||
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,
|
||||
OriginCert: originCert,
|
||||
// turn off use of reconnect token and auth refresh when using named tunnels
|
||||
@@ -227,20 +232,14 @@ func prepareTunnelConfig(
|
||||
if ingressRules.IsEmpty() {
|
||||
ingressRules, err = ingress.NewSingleOrigin(c, !isNamedTunnel)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var warpRoutingService *ingress.WarpRoutingService
|
||||
warpRoutingEnabled := isWarpRoutingEnabled(cfg.WarpRouting, isNamedTunnel)
|
||||
if warpRoutingEnabled {
|
||||
warpRoutingService = ingress.NewWarpRoutingService()
|
||||
log.Info().Msgf("Warp-routing is enabled")
|
||||
}
|
||||
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.ProtocolPercentage, origin.ResolveTTL, log)
|
||||
protocolSelector, err := connection.NewProtocolSelector(c.String("protocol"), warpRoutingEnabled, namedTunnel, edgediscovery.ProtocolPercentage, supervisor.ResolveTTL, log)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
return nil, nil, err
|
||||
}
|
||||
log.Info().Msgf("Initial protocol %s", protocolSelector.Current())
|
||||
|
||||
@@ -248,11 +247,11 @@ func prepareTunnelConfig(
|
||||
for _, p := range connection.ProtocolList {
|
||||
tlsSettings := p.TLSSettings()
|
||||
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)
|
||||
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 {
|
||||
edgeTLSConfig.NextProtos = tlsSettings.NextProtos
|
||||
@@ -260,15 +259,9 @@ func prepareTunnelConfig(
|
||||
edgeTLSConfigs[p] = edgeTLSConfig
|
||||
}
|
||||
|
||||
originProxy := origin.NewOriginProxy(ingressRules, warpRoutingService, tags, log)
|
||||
gracePeriod, err := gracePeriod(c)
|
||||
if err != nil {
|
||||
return nil, ingress.Ingress{}, err
|
||||
}
|
||||
connectionConfig := &connection.Config{
|
||||
OriginProxy: originProxy,
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
return nil, nil, err
|
||||
}
|
||||
muxerConfig := &connection.MuxerConfig{
|
||||
HeartbeatInterval: c.Duration("heartbeat-interval"),
|
||||
@@ -279,21 +272,22 @@ func prepareTunnelConfig(
|
||||
MetricsUpdateFreq: c.Duration("metrics-update-freq"),
|
||||
}
|
||||
|
||||
return &origin.TunnelConfig{
|
||||
ConnectionConfig: connectionConfig,
|
||||
OSArch: info.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: origin.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
LogTransport: logTransport,
|
||||
Observer: observer,
|
||||
ReportedVersion: info.Version(),
|
||||
tunnelConfig := &supervisor.TunnelConfig{
|
||||
GracePeriod: gracePeriod,
|
||||
ReplaceExisting: c.Bool("force"),
|
||||
OSArch: info.OSArch(),
|
||||
ClientID: clientID,
|
||||
EdgeAddrs: c.StringSlice("edge"),
|
||||
Region: c.String("region"),
|
||||
HAConnections: c.Int("ha-connections"),
|
||||
IncidentLookup: supervisor.NewIncidentLookup(),
|
||||
IsAutoupdated: c.Bool("is-autoupdated"),
|
||||
LBPool: c.String("lb-pool"),
|
||||
Tags: tags,
|
||||
Log: log,
|
||||
LogTransport: logTransport,
|
||||
Observer: observer,
|
||||
ReportedVersion: info.Version(),
|
||||
// Note TUN-3758 , we use Int because UInt is not supported with altsrc
|
||||
Retries: uint(c.Int("retries")),
|
||||
RunFromTerminal: isRunningFromTerminal(),
|
||||
@@ -302,7 +296,12 @@ func prepareTunnelConfig(
|
||||
MuxerConfig: muxerConfig,
|
||||
ProtocolSelector: protocolSelector,
|
||||
EdgeTLSConfigs: edgeTLSConfigs,
|
||||
}, ingressRules, nil
|
||||
}
|
||||
dynamicConfig := &orchestration.Config{
|
||||
Ingress: &ingressRules,
|
||||
WarpRoutingEnabled: warpRoutingEnabled,
|
||||
}
|
||||
return tunnelConfig, dynamicConfig, nil
|
||||
}
|
||||
|
||||
func gracePeriod(c *cli.Context) (time.Duration, error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
@@ -12,6 +13,15 @@ import (
|
||||
"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 {
|
||||
return &cli.Command{
|
||||
Name: "ingress",
|
||||
@@ -49,6 +59,7 @@ func buildValidateIngressCommand() *cli.Command {
|
||||
Usage: "Validate the ingress configuration ",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] ingress validate",
|
||||
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
|
||||
func validateIngressCommand(c *cli.Context, warnings string) error {
|
||||
conf := config.GetConfiguration()
|
||||
if conf.Source() == "" {
|
||||
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 nil
|
||||
conf, err := getConfiguration(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Validating rules from", conf.Source())
|
||||
|
||||
if _, err := ingress.ParseIngress(conf); err != nil {
|
||||
return errors.Wrap(err, "Validation failed")
|
||||
}
|
||||
@@ -90,6 +100,22 @@ func validateIngressCommand(c *cli.Context, warnings string) error {
|
||||
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.
|
||||
func testURLCommand(c *cli.Context) error {
|
||||
requestArg := c.Args().First()
|
||||
|
||||
@@ -55,7 +55,6 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
AccountTag: data.Result.AccountTag,
|
||||
TunnelSecret: data.Result.Secret,
|
||||
TunnelID: tunnelID,
|
||||
TunnelName: data.Result.Name,
|
||||
}
|
||||
|
||||
url := data.Result.Hostname
|
||||
@@ -77,7 +76,7 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
return StartServer(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
&connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
|
||||
@@ -185,7 +185,6 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||
AccountTag: credential.cert.AccountID,
|
||||
TunnelSecret: tunnelSecret,
|
||||
TunnelID: tunnel.ID,
|
||||
TunnelName: name,
|
||||
}
|
||||
usedCertPath := false
|
||||
if credentialsFilePath == "" {
|
||||
@@ -221,7 +220,8 @@ func (sc *subcommandContext) create(name string, credentialsFilePath string, sec
|
||||
}
|
||||
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)
|
||||
return tunnel, nil
|
||||
|
||||
return &tunnel.Tunnel, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
|
||||
@@ -301,10 +301,16 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
|
||||
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(
|
||||
sc.c,
|
||||
buildInfo,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials},
|
||||
&connection.NamedTunnelProperties{Credentials: credentials},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
@@ -370,7 +376,7 @@ func (sc *subcommandContext) findID(input string) (uuid.UUID, error) {
|
||||
// Look up name in the credentials file.
|
||||
credFinder := newStaticPath(sc.c.String(CredFileFlag), sc.fs)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
@@ -115,7 +116,6 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
TunnelSecret: secret,
|
||||
TunnelName: name,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -160,7 +160,6 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
AccountTag: accountTag,
|
||||
TunnelID: tunnelID,
|
||||
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 (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -34,6 +35,7 @@ const (
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
CredContentsFlag = "credentials-contents"
|
||||
TunnelTokenFlag = "token"
|
||||
overwriteDNSFlagName = "overwrite-dns"
|
||||
|
||||
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.",
|
||||
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{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
@@ -597,6 +604,7 @@ func buildRunCommand() *cli.Command {
|
||||
credentialsContentsFlag,
|
||||
selectProtocolFlag,
|
||||
featuresFlag,
|
||||
tunnelTokenFlag,
|
||||
}
|
||||
flags = append(flags, configureProxyFlags(false)...)
|
||||
return &cli.Command{
|
||||
@@ -627,14 +635,6 @@ func runCommand(c *cli.Context) error {
|
||||
if c.NArg() > 1 {
|
||||
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") != "" {
|
||||
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.")
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -650,9 +681,6 @@ func runNamedTunnel(sc *subcommandContext, tunnelRef string) error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error parsing tunnel ID")
|
||||
}
|
||||
|
||||
sc.log.Info().Str(LogFieldTunnelID, tunnelID.String()).Msg("Starting tunnel")
|
||||
|
||||
return sc.run(tunnelID)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
var (
|
||||
vnetFlag = &cli.StringFlag{
|
||||
Name: "virtual-network",
|
||||
Name: "vnet",
|
||||
Aliases: []string{"vn"},
|
||||
Usage: "The ID or name of the virtual network to which the route is associated to.",
|
||||
}
|
||||
@@ -35,7 +35,7 @@ determine who can reach certain routes.
|
||||
By default IP routes all exist within a single virtual network. If you use the same IP
|
||||
space(s) in different physical private networks, all meant to be reachable via IP routes,
|
||||
then you have to manage the ambiguous IP routes by associating them to virtual networks.
|
||||
See "cloudflared tunnel network --help" for more information.`,
|
||||
See "cloudflared tunnel vnet --help" for more information.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "add",
|
||||
@@ -50,9 +50,9 @@ the specified Tunnel, and reach an IP in the given CIDR, as long as that IP is
|
||||
reachable from cloudflared.
|
||||
If the CIDR exists in more than one private network, to be connected with Cloudflare
|
||||
Tunnels, then you have to manage those IP routes with virtual networks (see
|
||||
"cloudflared tunnel network --help)". In those cases, you then have to tell
|
||||
"cloudflared tunnel vnet --help)". In those cases, you then have to tell
|
||||
which virtual network's routing table you want to add the route to with:
|
||||
"cloudflared tunnel route ip add --virtual-network [ID/name] [CIDR] [TUNNEL]".`,
|
||||
"cloudflared tunnel route ip add --vnet [ID/name] [CIDR] [TUNNEL]".`,
|
||||
Flags: []cli.Flag{vnetFlag},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -37,7 +37,7 @@ var (
|
||||
|
||||
func buildVirtualNetworkSubcommand(hidden bool) *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "network",
|
||||
Name: "vnet",
|
||||
Usage: "Configure and query virtual networks to manage private IP routes with overlapping IPs.",
|
||||
UsageText: "cloudflared tunnel [--config FILEPATH] network COMMAND [arguments...]",
|
||||
Description: `cloudflared allows to manage IP routes that expose origins in your private network space via their IP directly
|
||||
@@ -177,7 +177,7 @@ func listVirtualNetworksCommand(c *cli.Context) error {
|
||||
if len(vnets) > 0 {
|
||||
formatAndPrintVnetsList(vnets)
|
||||
} else {
|
||||
fmt.Println("No virtual networks were found for the given filter flags. You can use 'cloudflared tunnel network add' to add a virtual network.")
|
||||
fmt.Println("No virtual networks were found for the given filter flags. You can use 'cloudflared tunnel vnet add' to add a virtual network.")
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
const (
|
||||
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."
|
||||
noUpdateManagedPackageMessage = "cloudflared will not automatically update if installed by a package manager."
|
||||
isManagedInstallFile = ".installedFromPackageManager"
|
||||
@@ -30,7 +30,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
version string
|
||||
version string
|
||||
BuiltForPackageManager = ""
|
||||
)
|
||||
|
||||
// BinaryUpdated implements ExitCoder interface, the app will exit with status code 11
|
||||
@@ -118,7 +119,11 @@ func Update(c *cli.Context) error {
|
||||
log := logger.CreateLoggerFromContext(c, logger.EnableTerminalLog)
|
||||
|
||||
if wasInstalledFromPackageManager() {
|
||||
log.Error().Msg("cloudflared was installed by a package manager. Please update using the same method.")
|
||||
packageManagerName := "a package manager"
|
||||
if BuiltForPackageManager != "" {
|
||||
packageManagerName = BuiltForPackageManager
|
||||
}
|
||||
log.Error().Msg(fmt.Sprintf("cloudflared was installed by %s. Please update using the same method.", packageManagerName))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -281,7 +286,7 @@ func supportAutoUpdate(log *zerolog.Logger) bool {
|
||||
|
||||
func wasInstalledFromPackageManager() bool {
|
||||
ok, _ := config.FileExists(filepath.Join(config.DefaultUnixConfigLocation, isManagedInstallFile))
|
||||
return ok
|
||||
return len(BuiltForPackageManager) != 0 || ok
|
||||
}
|
||||
|
||||
func isRunningFromTerminal() bool {
|
||||
|
||||
@@ -193,8 +193,15 @@ func installWindowsService(c *cli.Context) error {
|
||||
s.Close()
|
||||
return fmt.Errorf("Service %s already exists", windowsServiceName)
|
||||
}
|
||||
extraArgs, err := getServiceExtraArgsFromCliArgs(c, &log)
|
||||
if err != nil {
|
||||
errMsg := "Unable to determine extra arguments for windows service"
|
||||
log.Err(err).Msg(errMsg)
|
||||
return errors.Wrap(err, errMsg)
|
||||
}
|
||||
|
||||
config := mgr.Config{StartType: mgr.StartAutomatic, DisplayName: windowsServiceDescription}
|
||||
s, err = m.CreateService(windowsServiceName, exepath, config)
|
||||
s, err = m.CreateService(windowsServiceName, exepath, config, extraArgs...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot install service")
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ classic_hostname: "classic-tunnel-component-tests.example.com"
|
||||
origincert: "/Users/tunnel/.cloudflared/cert.pem"
|
||||
ingress:
|
||||
- hostname: named-tunnel-component-tests.example.com
|
||||
service: http_status:200
|
||||
service: hello_world
|
||||
- service: http_status:404
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python
|
||||
import copy
|
||||
import json
|
||||
import base64
|
||||
|
||||
from dataclasses import dataclass, InitVar
|
||||
|
||||
@@ -61,6 +63,23 @@ class NamedTunnelConfig(NamedTunnelBaseConfig):
|
||||
def get_url(self):
|
||||
return "https://" + self.ingress[0]['hostname']
|
||||
|
||||
def base_config(self):
|
||||
config = self.full_config.copy()
|
||||
|
||||
# removes the tunnel reference
|
||||
del(config["tunnel"])
|
||||
del(config["credentials-file"])
|
||||
|
||||
return config
|
||||
|
||||
def get_token(self):
|
||||
with open(self.credentials_file) as json_file:
|
||||
creds = json.load(json_file)
|
||||
token_dict = {"a": creds["AccountTag"], "t": creds["TunnelID"], "s": creds["TunnelSecret"]}
|
||||
token_json_str = json.dumps(token_dict)
|
||||
|
||||
return base64.b64encode(token_json_str.encode('utf-8'))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ClassicTunnelBaseConfig(BaseConfig):
|
||||
|
||||
@@ -3,3 +3,7 @@ MAX_RETRIES = 5
|
||||
BACKOFF_SECS = 7
|
||||
|
||||
PROXY_DNS_PORT = 9053
|
||||
|
||||
|
||||
def protocols():
|
||||
return ["h2mux", "http2", "quic"]
|
||||
|
||||
@@ -7,6 +7,7 @@ import pytest
|
||||
from flaky import flaky
|
||||
|
||||
from conftest import CfdModes
|
||||
from constants import protocols
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
@@ -18,9 +19,16 @@ class TestReconnect:
|
||||
"stdin-control": True,
|
||||
}
|
||||
|
||||
def _extra_config(self, protocol):
|
||||
return {
|
||||
"stdin-control": True,
|
||||
"protocol": protocol,
|
||||
}
|
||||
|
||||
@pytest.mark.skipif(platform.system() == "Windows", reason=f"Currently buggy on Windows TUN-4584")
|
||||
def test_named_reconnect(self, tmp_path, component_tests_config):
|
||||
config = component_tests_config(self.extra_config)
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_named_reconnect(self, tmp_path, component_tests_config, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(tmp_path, config, new_process=True, allow_input=True, capture_output=False) as cloudflared:
|
||||
# Repeat the test multiple times because some issues only occur after multiple reconnects
|
||||
self.assert_reconnect(config, cloudflared, 5)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import pathlib
|
||||
import platform
|
||||
import subprocess
|
||||
from contextlib import contextmanager
|
||||
@@ -9,7 +10,7 @@ import pytest
|
||||
|
||||
import test_logging
|
||||
from conftest import CfdModes
|
||||
from util import start_cloudflared, wait_tunnel_ready
|
||||
from util import start_cloudflared, wait_tunnel_ready, write_config
|
||||
|
||||
|
||||
def select_platform(plat):
|
||||
@@ -43,6 +44,21 @@ class TestServiceMode:
|
||||
|
||||
self.launchd_service_scenario(config, assert_log_file)
|
||||
|
||||
@select_platform("Darwin")
|
||||
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
|
||||
def test_launchd_service_with_token(self, tmp_path, component_tests_config):
|
||||
log_file = tmp_path / test_logging.default_log_file
|
||||
additional_config = {
|
||||
"logfile": str(log_file),
|
||||
}
|
||||
config = component_tests_config(additional_config=additional_config)
|
||||
|
||||
# service install doesn't install the config file but in this case we want to use some default settings
|
||||
# so we write the base config without the tunnel credentials and ID
|
||||
write_config(pathlib.Path(default_config_dir()), config.base_config())
|
||||
|
||||
self.launchd_service_scenario(config, use_token=True)
|
||||
|
||||
@select_platform("Darwin")
|
||||
@pytest.mark.skipif(os.path.exists(default_config_file()), reason=f"There is already a config file in default path")
|
||||
def test_launchd_service_rotating_log(self, tmp_path, component_tests_config):
|
||||
@@ -60,12 +76,13 @@ class TestServiceMode:
|
||||
|
||||
self.launchd_service_scenario(config, assert_rotating_log)
|
||||
|
||||
def launchd_service_scenario(self, config, extra_assertions):
|
||||
with self.run_service(Path(default_config_dir()), config):
|
||||
def launchd_service_scenario(self, config, extra_assertions=None, use_token=False):
|
||||
with self.run_service(Path(default_config_dir()), config, use_token=use_token):
|
||||
self.launchctl_cmd("list")
|
||||
self.launchctl_cmd("start")
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
extra_assertions()
|
||||
if extra_assertions is not None:
|
||||
extra_assertions()
|
||||
self.launchctl_cmd("stop")
|
||||
|
||||
os.remove(default_config_file())
|
||||
@@ -105,12 +122,30 @@ class TestServiceMode:
|
||||
|
||||
self.sysv_service_scenario(config, tmp_path, assert_rotating_log)
|
||||
|
||||
def sysv_service_scenario(self, config, tmp_path, extra_assertions):
|
||||
with self.run_service(tmp_path, config, root=True):
|
||||
@select_platform("Linux")
|
||||
@pytest.mark.skipif(os.path.exists("/etc/cloudflared/config.yml"),
|
||||
reason=f"There is already a config file in default path")
|
||||
def test_sysv_service_with_token(self, tmp_path, component_tests_config):
|
||||
additional_config = {
|
||||
"loglevel": "debug",
|
||||
}
|
||||
|
||||
config = component_tests_config(additional_config=additional_config)
|
||||
|
||||
# service install doesn't install the config file but in this case we want to use some default settings
|
||||
# so we write the base config without the tunnel credentials and ID
|
||||
config_path = write_config(tmp_path, config.base_config())
|
||||
subprocess.run(["sudo", "cp", config_path, "/etc/cloudflared/config.yml"], check=True)
|
||||
|
||||
self.sysv_service_scenario(config, tmp_path, use_token=True)
|
||||
|
||||
def sysv_service_scenario(self, config, tmp_path, extra_assertions=None, use_token=False):
|
||||
with self.run_service(tmp_path, config, root=True, use_token=use_token):
|
||||
self.sysv_cmd("start")
|
||||
self.sysv_cmd("status")
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
extra_assertions()
|
||||
if extra_assertions is not None:
|
||||
extra_assertions()
|
||||
self.sysv_cmd("stop")
|
||||
|
||||
# Service install copies config file to /etc/cloudflared/config.yml
|
||||
@@ -118,14 +153,19 @@ class TestServiceMode:
|
||||
self.sysv_cmd("status", success=False)
|
||||
|
||||
@contextmanager
|
||||
def run_service(self, tmp_path, config, root=False):
|
||||
def run_service(self, tmp_path, config, root=False, use_token=False):
|
||||
args = ["service", "install"]
|
||||
|
||||
if use_token:
|
||||
args.append(config.get_token())
|
||||
|
||||
try:
|
||||
service = start_cloudflared(
|
||||
tmp_path, config, cfd_args=["service", "install"], cfd_pre_args=[], capture_output=False, root=root)
|
||||
tmp_path, config, cfd_args=args, cfd_pre_args=[], capture_output=False, root=root, skip_config_flag=use_token)
|
||||
yield service
|
||||
finally:
|
||||
start_cloudflared(
|
||||
tmp_path, config, cfd_args=["service", "uninstall"], cfd_pre_args=[], capture_output=False, root=root)
|
||||
tmp_path, config, cfd_args=["service", "uninstall"], cfd_pre_args=[], capture_output=False, root=root, skip_config_flag=use_token)
|
||||
|
||||
def launchctl_cmd(self, action, success=True):
|
||||
cmd = subprocess.run(
|
||||
|
||||
@@ -8,6 +8,7 @@ import time
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from constants import protocols
|
||||
from util import start_cloudflared, wait_tunnel_ready, check_tunnel_not_connected
|
||||
|
||||
|
||||
@@ -17,17 +18,21 @@ def supported_signals():
|
||||
return [signal.SIGTERM, signal.SIGINT]
|
||||
|
||||
|
||||
class TestTermination():
|
||||
class TestTermination:
|
||||
grace_period = 5
|
||||
timeout = 10
|
||||
extra_config = {
|
||||
"grace-period": f"{grace_period}s",
|
||||
}
|
||||
sse_endpoint = "/sse?freq=1s"
|
||||
|
||||
def _extra_config(self, protocol):
|
||||
return {
|
||||
"grace-period": f"{self.grace_period}s",
|
||||
"protocol": protocol,
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("signal", supported_signals())
|
||||
def test_graceful_shutdown(self, tmp_path, component_tests_config, signal):
|
||||
config = component_tests_config(self.extra_config)
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_graceful_shutdown(self, tmp_path, component_tests_config, signal, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
@@ -47,8 +52,9 @@ class TestTermination():
|
||||
# test cloudflared terminates before grace period expires when all eyeball
|
||||
# connections are drained
|
||||
@pytest.mark.parametrize("signal", supported_signals())
|
||||
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal):
|
||||
config = component_tests_config(self.extra_config)
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_shutdown_once_no_connection(self, tmp_path, component_tests_config, signal, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
@@ -66,8 +72,9 @@ class TestTermination():
|
||||
self.wait_eyeball_thread(in_flight_req, self.grace_period)
|
||||
|
||||
@pytest.mark.parametrize("signal", supported_signals())
|
||||
def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal):
|
||||
config = component_tests_config(self.extra_config)
|
||||
@pytest.mark.parametrize("protocol", protocols())
|
||||
def test_no_connection_shutdown(self, tmp_path, component_tests_config, signal, protocol):
|
||||
config = component_tests_config(self._extra_config(protocol))
|
||||
with start_cloudflared(
|
||||
tmp_path, config, new_process=True, capture_output=False) as cloudflared:
|
||||
wait_tunnel_ready(tunnel_url=config.get_url())
|
||||
|
||||
+11
-3
@@ -21,9 +21,14 @@ def write_config(directory, config):
|
||||
|
||||
|
||||
def start_cloudflared(directory, config, cfd_args=["run"], cfd_pre_args=["tunnel"], new_process=False,
|
||||
allow_input=False, capture_output=True, root=False):
|
||||
config_path = write_config(directory, config.full_config)
|
||||
allow_input=False, capture_output=True, root=False, skip_config_flag=False):
|
||||
|
||||
config_path = None
|
||||
if not skip_config_flag:
|
||||
config_path = write_config(directory, config.full_config)
|
||||
|
||||
cmd = cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root)
|
||||
|
||||
if new_process:
|
||||
return run_cloudflared_background(cmd, allow_input, capture_output)
|
||||
# By setting check=True, it will raise an exception if the process exits with non-zero exit code
|
||||
@@ -36,7 +41,10 @@ def cloudflared_cmd(config, config_path, cfd_args, cfd_pre_args, root):
|
||||
cmd += ["sudo"]
|
||||
cmd += [config.cloudflared_binary]
|
||||
cmd += cfd_pre_args
|
||||
cmd += ["--config", str(config_path)]
|
||||
|
||||
if config_path is not None:
|
||||
cmd += ["--config", str(config_path)]
|
||||
|
||||
cmd += cfd_args
|
||||
LOGGER.info(f"Run cmd {cmd} with config {config}")
|
||||
return cmd
|
||||
|
||||
+62
-27
@@ -1,12 +1,14 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
@@ -49,7 +51,7 @@ func DefaultConfigDirectory() string {
|
||||
path := os.Getenv("CFDPATH")
|
||||
if path == "" {
|
||||
path = filepath.Join(os.Getenv("ProgramFiles(x86)"), "cloudflared")
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) { //doesn't exist, so return an empty failure string
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) { // doesn't exist, so return an empty failure string
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -138,7 +140,7 @@ func FindOrCreateConfigPath() string {
|
||||
defer file.Close()
|
||||
|
||||
logDir := DefaultLogDirectory()
|
||||
_ = os.MkdirAll(logDir, os.ModePerm) //try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
|
||||
_ = os.MkdirAll(logDir, os.ModePerm) // try and create it. Doesn't matter if it succeed or not, only byproduct will be no logs
|
||||
|
||||
c := Root{
|
||||
LogDirectory: logDir,
|
||||
@@ -175,60 +177,62 @@ func ValidateUrl(c *cli.Context, allowURLFromArgs bool) (*url.URL, error) {
|
||||
}
|
||||
|
||||
type UnvalidatedIngressRule struct {
|
||||
Hostname string
|
||||
Path string
|
||||
Service string
|
||||
OriginRequest OriginRequestConfig `yaml:"originRequest"`
|
||||
Hostname string `json:"hostname"`
|
||||
Path string `json:"path"`
|
||||
Service string `json:"service"`
|
||||
OriginRequest OriginRequestConfig `yaml:"originRequest" json:"originRequest"`
|
||||
}
|
||||
|
||||
// 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
|
||||
// up general config that apply to all rules, and also, specific per-rule
|
||||
// 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 {
|
||||
// HTTP proxy timeout for establishing a new connection
|
||||
ConnectTimeout *time.Duration `yaml:"connectTimeout"`
|
||||
ConnectTimeout *CustomDuration `yaml:"connectTimeout" json:"connectTimeout"`
|
||||
// HTTP proxy timeout for completing a TLS handshake
|
||||
TLSTimeout *time.Duration `yaml:"tlsTimeout"`
|
||||
TLSTimeout *CustomDuration `yaml:"tlsTimeout" json:"tlsTimeout"`
|
||||
// HTTP proxy TCP keepalive duration
|
||||
TCPKeepAlive *time.Duration `yaml:"tcpKeepAlive"`
|
||||
TCPKeepAlive *CustomDuration `yaml:"tcpKeepAlive" json:"tcpKeepAlive"`
|
||||
// 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
|
||||
KeepAliveConnections *int `yaml:"keepAliveConnections"`
|
||||
KeepAliveConnections *int `yaml:"keepAliveConnections" json:"keepAliveConnections"`
|
||||
// HTTP proxy timeout for closing an idle connection
|
||||
KeepAliveTimeout *time.Duration `yaml:"keepAliveTimeout"`
|
||||
KeepAliveTimeout *CustomDuration `yaml:"keepAliveTimeout" json:"keepAliveTimeout"`
|
||||
// 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.
|
||||
OriginServerName *string `yaml:"originServerName"`
|
||||
OriginServerName *string `yaml:"originServerName" json:"originServerName"`
|
||||
// Path to the CA for the certificate of your origin.
|
||||
// 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.
|
||||
// Will allow any certificate from the origin to be accepted.
|
||||
// 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.
|
||||
// Useful if you are running a WSGI server.
|
||||
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding"`
|
||||
DisableChunkedEncoding *bool `yaml:"disableChunkedEncoding" json:"disableChunkedEncoding"`
|
||||
// Runs as jump host
|
||||
BastionMode *bool `yaml:"bastionMode"`
|
||||
BastionMode *bool `yaml:"bastionMode" json:"bastionMode"`
|
||||
// Listen address for the proxy.
|
||||
ProxyAddress *string `yaml:"proxyAddress"`
|
||||
ProxyAddress *string `yaml:"proxyAddress" json:"proxyAddress"`
|
||||
// Listen port for the proxy.
|
||||
ProxyPort *uint `yaml:"proxyPort"`
|
||||
ProxyPort *uint `yaml:"proxyPort" json:"proxyPort"`
|
||||
// Valid options are 'socks' or empty.
|
||||
ProxyType *string `yaml:"proxyType"`
|
||||
ProxyType *string `yaml:"proxyType" json:"proxyType"`
|
||||
// IP rules for the proxy service
|
||||
IPRules []IngressIPRule `yaml:"ipRules"`
|
||||
IPRules []IngressIPRule `yaml:"ipRules" json:"ipRules"`
|
||||
}
|
||||
|
||||
type IngressIPRule struct {
|
||||
Prefix *string `yaml:"prefix"`
|
||||
Ports []int `yaml:"ports"`
|
||||
Allow bool `yaml:"allow"`
|
||||
Prefix *string `yaml:"prefix" json:"prefix"`
|
||||
Ports []int `yaml:"ports" json:"ports"`
|
||||
Allow bool `yaml:"allow" json:"allow"`
|
||||
}
|
||||
|
||||
type Configuration struct {
|
||||
@@ -240,7 +244,7 @@ type Configuration struct {
|
||||
}
|
||||
|
||||
type WarpRoutingConfig struct {
|
||||
Enabled bool `yaml:"enabled"`
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
}
|
||||
|
||||
type configFileSettings struct {
|
||||
@@ -397,3 +401,34 @@ func ReadConfigFile(c *cli.Context, log *zerolog.Logger) (settings *configFileSe
|
||||
|
||||
return &configuration, warnings, nil
|
||||
}
|
||||
|
||||
// A CustomDuration is a Duration that has custom serialization for JSON.
|
||||
// JSON in Javascript assumes that int fields are 32 bits and Duration fields are deserialized assuming that numbers
|
||||
// are in nanoseconds, which in 32bit integers limits to just 2 seconds.
|
||||
// This type assumes that when serializing/deserializing from JSON, that the number is in seconds, while it maintains
|
||||
// the YAML serde assumptions.
|
||||
type CustomDuration struct {
|
||||
time.Duration
|
||||
}
|
||||
|
||||
func (s *CustomDuration) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.Duration.Seconds())
|
||||
}
|
||||
|
||||
func (s *CustomDuration) UnmarshalJSON(data []byte) error {
|
||||
seconds, err := strconv.ParseInt(string(data), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Duration = time.Duration(seconds * int64(time.Second))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CustomDuration) MarshalYAML() (interface{}, error) {
|
||||
return s.Duration.String(), nil
|
||||
}
|
||||
|
||||
func (s *CustomDuration) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
return unmarshal(&s.Duration)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,18 @@ func TestConfigFileSettings(t *testing.T) {
|
||||
)
|
||||
rawYAML := `
|
||||
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:
|
||||
- hostname: tunnel1.example.com
|
||||
path: /id
|
||||
@@ -53,6 +67,21 @@ counters:
|
||||
assert.Equal(t, firstIngress, config.Ingress[0])
|
||||
assert.Equal(t, secondIngress, config.Ingress[1])
|
||||
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")
|
||||
assert.NoError(t, err)
|
||||
@@ -81,3 +110,106 @@ counters:
|
||||
assert.Equal(t, 456, counters[1])
|
||||
|
||||
}
|
||||
|
||||
var rawConfig = []byte(`
|
||||
{
|
||||
"connectTimeout": 10,
|
||||
"tlsTimeout": 30,
|
||||
"tcpKeepAlive": 30,
|
||||
"noHappyEyeballs": true,
|
||||
"keepAliveTimeout": 60,
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
`)
|
||||
|
||||
func TestMarshalUnmarshalOriginRequest(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
marshalFunc func(in interface{}) (out []byte, err error)
|
||||
unMarshalFunc func(in []byte, out interface{}) (err error)
|
||||
baseUnit time.Duration
|
||||
}{
|
||||
{"json", json.Marshal, json.Unmarshal, time.Second},
|
||||
{"yaml", yaml.Marshal, yaml.Unmarshal, time.Nanosecond},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assertConfig(t, tc.marshalFunc, tc.unMarshalFunc, tc.baseUnit)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertConfig(
|
||||
t *testing.T,
|
||||
marshalFunc func(in interface{}) (out []byte, err error),
|
||||
unMarshalFunc func(in []byte, out interface{}) (err error),
|
||||
baseUnit time.Duration,
|
||||
) {
|
||||
var config OriginRequestConfig
|
||||
var config2 OriginRequestConfig
|
||||
|
||||
assert.NoError(t, unMarshalFunc(rawConfig, &config))
|
||||
|
||||
assert.Equal(t, baseUnit*10, config.ConnectTimeout.Duration)
|
||||
assert.Equal(t, baseUnit*30, config.TLSTimeout.Duration)
|
||||
assert.Equal(t, baseUnit*30, config.TCPKeepAlive.Duration)
|
||||
assert.Equal(t, true, *config.NoHappyEyeballs)
|
||||
assert.Equal(t, baseUnit*60, config.KeepAliveTimeout.Duration)
|
||||
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)
|
||||
|
||||
// validate that serializing and deserializing again matches the deserialization from raw string
|
||||
result, err := marshalFunc(config)
|
||||
require.NoError(t, err)
|
||||
err = unMarshalFunc(result, &config2)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, config2, config)
|
||||
}
|
||||
|
||||
@@ -25,13 +25,12 @@ const (
|
||||
|
||||
var switchingProtocolText = fmt.Sprintf("%d %s", http.StatusSwitchingProtocols, http.StatusText(http.StatusSwitchingProtocols))
|
||||
|
||||
type Config struct {
|
||||
OriginProxy OriginProxy
|
||||
GracePeriod time.Duration
|
||||
ReplaceExisting bool
|
||||
type Orchestrator interface {
|
||||
UpdateConfig(version int32, config []byte) *pogs.UpdateConfigurationResponse
|
||||
GetOriginProxy() (OriginProxy, error)
|
||||
}
|
||||
|
||||
type NamedTunnelConfig struct {
|
||||
type NamedTunnelProperties struct {
|
||||
Credentials Credentials
|
||||
Client pogs.ClientInfo
|
||||
QuickTunnelUrl string
|
||||
@@ -42,7 +41,6 @@ type Credentials struct {
|
||||
AccountTag string
|
||||
TunnelSecret []byte
|
||||
TunnelID uuid.UUID
|
||||
TunnelName string
|
||||
}
|
||||
|
||||
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
|
||||
OriginCert []byte
|
||||
// feature-flag to use new edge reconnect tokens
|
||||
@@ -67,6 +80,7 @@ const (
|
||||
TypeTCP
|
||||
TypeControlStream
|
||||
TypeHTTP
|
||||
TypeConfiguration
|
||||
)
|
||||
|
||||
// ShouldFlush returns whether this kind of connection should actively flush data
|
||||
|
||||
@@ -4,33 +4,28 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gobwas/ws/wsutil"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/ingress"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
"github.com/cloudflare/cloudflared/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
largeFileSize = 2 * 1024 * 1024
|
||||
largeFileSize = 2 * 1024 * 1024
|
||||
testGracePeriod = time.Millisecond * 100
|
||||
)
|
||||
|
||||
var (
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
testConfig = &Config{
|
||||
OriginProxy: &mockOriginProxy{},
|
||||
GracePeriod: time.Millisecond * 100,
|
||||
testOrchestrator = &mockOrchestrator{
|
||||
originProxy: &mockOriginProxy{},
|
||||
}
|
||||
log = zerolog.Nop()
|
||||
testOriginURL = &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "connectiontest.argotunnel.com",
|
||||
}
|
||||
testLargeResp = make([]byte, largeFileSize)
|
||||
)
|
||||
|
||||
@@ -42,6 +37,20 @@ type testRequest struct {
|
||||
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{}
|
||||
|
||||
func (moc *mockOriginProxy) ProxyHTTP(
|
||||
@@ -50,7 +59,15 @@ func (moc *mockOriginProxy) ProxyHTTP(
|
||||
isWebsocket bool,
|
||||
) error {
|
||||
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 {
|
||||
case "/ok":
|
||||
@@ -78,32 +95,82 @@ func (moc *mockOriginProxy) ProxyTCP(
|
||||
return nil
|
||||
}
|
||||
|
||||
type nowriter struct {
|
||||
io.Reader
|
||||
type echoPipe struct {
|
||||
reader *io.PipeReader
|
||||
writer *io.PipeWriter
|
||||
}
|
||||
|
||||
func (nowriter) Write(p []byte) (int, error) {
|
||||
return 0, fmt.Errorf("Writer not implemented")
|
||||
func (ep *echoPipe) Read(p []byte) (int, error) {
|
||||
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{
|
||||
StatusCode: http.StatusSwitchingProtocols,
|
||||
}
|
||||
_ = w.WriteRespHeaders(resp.StatusCode, resp.Header)
|
||||
clientReader := nowriter{r.Body}
|
||||
if err := w.WriteRespHeaders(resp.StatusCode, resp.Header); err != nil {
|
||||
return err
|
||||
}
|
||||
wsCtx, cancel := context.WithCancel(r.Context())
|
||||
readPipe, writePipe := io.Pipe()
|
||||
wsConn := websocket.NewConn(wsCtx, NewHTTPResponseReadWriterAcker(w, r), &log)
|
||||
go func() {
|
||||
for {
|
||||
data, err := wsutil.ReadClientText(clientReader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := wsutil.WriteServerText(w, data); err != nil {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-wsCtx.Done():
|
||||
case <-r.Context().Done():
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+16
-20
@@ -16,9 +16,9 @@ type RPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) Na
|
||||
type controlStream struct {
|
||||
observer *Observer
|
||||
|
||||
connectedFuse ConnectedFuse
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
connIndex uint8
|
||||
connectedFuse ConnectedFuse
|
||||
namedTunnelProperties *NamedTunnelProperties
|
||||
connIndex uint8
|
||||
|
||||
newRPCClientFunc RPCClientFunc
|
||||
|
||||
@@ -29,7 +29,9 @@ type controlStream struct {
|
||||
|
||||
// ControlStreamHandler registers connections with origintunneld and initiates graceful shutdown.
|
||||
type ControlStreamHandler interface {
|
||||
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, shouldWaitForUnregister bool) error
|
||||
// ServeControlStream handles the control plane of the transport in the current goroutine calling this
|
||||
ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions) error
|
||||
// IsStopped tells whether the method above has finished
|
||||
IsStopped() bool
|
||||
}
|
||||
|
||||
@@ -37,7 +39,7 @@ type ControlStreamHandler interface {
|
||||
func NewControlStream(
|
||||
observer *Observer,
|
||||
connectedFuse ConnectedFuse,
|
||||
namedTunnelConfig *NamedTunnelConfig,
|
||||
namedTunnelConfig *NamedTunnelProperties,
|
||||
connIndex uint8,
|
||||
newRPCClientFunc RPCClientFunc,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
@@ -47,13 +49,13 @@ func NewControlStream(
|
||||
newRPCClientFunc = newRegistrationRPCClient
|
||||
}
|
||||
return &controlStream{
|
||||
observer: observer,
|
||||
connectedFuse: connectedFuse,
|
||||
namedTunnelConfig: namedTunnelConfig,
|
||||
newRPCClientFunc: newRPCClientFunc,
|
||||
connIndex: connIndex,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
gracePeriod: gracePeriod,
|
||||
observer: observer,
|
||||
connectedFuse: connectedFuse,
|
||||
namedTunnelProperties: namedTunnelConfig,
|
||||
newRPCClientFunc: newRPCClientFunc,
|
||||
connIndex: connIndex,
|
||||
gracefulShutdownC: gracefulShutdownC,
|
||||
gracePeriod: gracePeriod,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,22 +63,16 @@ func (c *controlStream) ServeControlStream(
|
||||
ctx context.Context,
|
||||
rw io.ReadWriteCloser,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
shouldWaitForUnregister bool,
|
||||
) error {
|
||||
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()
|
||||
return err
|
||||
}
|
||||
c.connectedFuse.Connected()
|
||||
|
||||
if shouldWaitForUnregister {
|
||||
c.waitForUnregister(ctx, rpcClient)
|
||||
} else {
|
||||
go c.waitForUnregister(ctx, rpcClient)
|
||||
}
|
||||
|
||||
c.waitForUnregister(ctx, rpcClient)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+17
-8
@@ -22,9 +22,10 @@ const (
|
||||
)
|
||||
|
||||
type h2muxConnection struct {
|
||||
config *Config
|
||||
muxerConfig *MuxerConfig
|
||||
muxer *h2mux.Muxer
|
||||
orchestrator Orchestrator
|
||||
gracePeriod time.Duration
|
||||
muxerConfig *MuxerConfig
|
||||
muxer *h2mux.Muxer
|
||||
// connectionID is only used by metrics, and prometheus requires labels to be string
|
||||
connIndexStr string
|
||||
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
|
||||
func NewH2muxConnection(
|
||||
config *Config,
|
||||
orchestrator Orchestrator,
|
||||
gracePeriod time.Duration,
|
||||
muxerConfig *MuxerConfig,
|
||||
edgeConn net.Conn,
|
||||
connIndex uint8,
|
||||
@@ -68,7 +70,8 @@ func NewH2muxConnection(
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) (*h2muxConnection, error, bool) {
|
||||
h := &h2muxConnection{
|
||||
config: config,
|
||||
orchestrator: orchestrator,
|
||||
gracePeriod: gracePeriod,
|
||||
muxerConfig: muxerConfig,
|
||||
connIndexStr: uint8ToString(connIndex),
|
||||
connIndex: connIndex,
|
||||
@@ -88,7 +91,7 @@ func NewH2muxConnection(
|
||||
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.Go(func() error {
|
||||
return h.serveMuxer(serveCtx)
|
||||
@@ -117,7 +120,7 @@ func (h *h2muxConnection) ServeNamedTunnel(ctx context.Context, namedTunnel *Nam
|
||||
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.Go(func() error {
|
||||
return h.serveMuxer(serveCtx)
|
||||
@@ -224,7 +227,13 @@ func (h *h2muxConnection) ServeStream(stream *h2mux.MuxedStream) error {
|
||||
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 {
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ func newH2MuxConnection(t require.TestingT) (*h2muxConnection, *h2mux.Muxer) {
|
||||
}()
|
||||
var connIndex = uint8(0)
|
||||
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)
|
||||
return h2muxConn, <-edgeMuxChan
|
||||
}
|
||||
@@ -147,7 +147,7 @@ func TestServeStreamWS(t *testing.T) {
|
||||
headers := []h2mux.Header{
|
||||
{
|
||||
Name: ":path",
|
||||
Value: "/ws",
|
||||
Value: "/ws/echo",
|
||||
},
|
||||
{
|
||||
Name: "connection",
|
||||
@@ -167,10 +167,10 @@ func TestServeStreamWS(t *testing.T) {
|
||||
assert.True(t, hasHeader(stream, ResponseMetaHeader, responseMetaHeaderOrigin))
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
err = wsutil.WriteClientBinary(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerText(stream)
|
||||
respBody, err := wsutil.ReadServerBinary(stream)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
|
||||
+59
-16
@@ -2,6 +2,7 @@ package connection
|
||||
|
||||
import (
|
||||
"context"
|
||||
gojson "encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
@@ -23,6 +24,7 @@ const (
|
||||
InternalTCPProxySrcHeader = "Cf-Cloudflared-Proxy-Src"
|
||||
WebsocketUpgrade = "websocket"
|
||||
ControlStreamUpgrade = "control-stream"
|
||||
ConfigurationUpdate = "update-configuration"
|
||||
)
|
||||
|
||||
var errEdgeConnectionClosed = fmt.Errorf("connection with edge closed")
|
||||
@@ -30,12 +32,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
|
||||
// origin.
|
||||
type HTTP2Connection struct {
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
config *Config
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndex uint8
|
||||
conn net.Conn
|
||||
server *http2.Server
|
||||
orchestrator Orchestrator
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
observer *Observer
|
||||
connIndex uint8
|
||||
// newRPCClientFunc allows us to mock RPCs during testing
|
||||
newRPCClientFunc func(context.Context, io.ReadWriteCloser, *zerolog.Logger) NamedTunnelRPCClient
|
||||
|
||||
@@ -49,7 +51,7 @@ type HTTP2Connection struct {
|
||||
// NewHTTP2Connection returns a new instance of HTTP2Connection.
|
||||
func NewHTTP2Connection(
|
||||
conn net.Conn,
|
||||
config *Config,
|
||||
orchestrator Orchestrator,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
observer *Observer,
|
||||
connIndex uint8,
|
||||
@@ -61,7 +63,7 @@ func NewHTTP2Connection(
|
||||
server: &http2.Server{
|
||||
MaxConcurrentStreams: MaxConcurrentStreams,
|
||||
},
|
||||
config: config,
|
||||
orchestrator: orchestrator,
|
||||
connOptions: connOptions,
|
||||
observer: observer,
|
||||
connIndex: connIndex,
|
||||
@@ -100,7 +102,13 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
connType := determineHTTP2Type(r)
|
||||
handleMissingRequestParts(connType, r)
|
||||
|
||||
respWriter, err := NewHTTP2RespWriter(r, w, connType)
|
||||
respWriter, err := NewHTTP2RespWriter(r, w, connType, c.log)
|
||||
if err != nil {
|
||||
c.observer.log.Error().Msg(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
originProxy, err := c.orchestrator.GetOriginProxy()
|
||||
if err != nil {
|
||||
c.observer.log.Error().Msg(err.Error())
|
||||
return
|
||||
@@ -108,15 +116,21 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch connType {
|
||||
case TypeControlStream:
|
||||
if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions, true); err != nil {
|
||||
if err := c.controlStreamHandler.ServeControlStream(r.Context(), respWriter, c.connOptions); err != nil {
|
||||
c.controlStreamErr = err
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
case TypeConfiguration:
|
||||
if err := c.handleConfigurationUpdate(respWriter, r); err != nil {
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
}
|
||||
|
||||
case TypeWebsocket, TypeHTTP:
|
||||
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)
|
||||
c.log.Error().Err(err)
|
||||
respWriter.WriteErrorResponse()
|
||||
@@ -131,7 +145,7 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
rws := NewHTTPResponseReadWriterAcker(respWriter, r)
|
||||
if err := c.config.OriginProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
if err := originProxy.ProxyTCP(r.Context(), rws, &TCPRequest{
|
||||
Dest: host,
|
||||
CFRay: FindCfRayHeader(r),
|
||||
LBProbe: IsLBProbeRequest(r),
|
||||
@@ -146,6 +160,26 @@ func (c *HTTP2Connection) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// ConfigurationUpdateBody is the representation followed by the edge to send updates to cloudflared.
|
||||
type ConfigurationUpdateBody struct {
|
||||
Version int32 `json:"version"`
|
||||
Config gojson.RawMessage `json:"config"`
|
||||
}
|
||||
|
||||
func (c *HTTP2Connection) handleConfigurationUpdate(respWriter *http2RespWriter, r *http.Request) error {
|
||||
var configBody ConfigurationUpdateBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&configBody); err != nil {
|
||||
return err
|
||||
}
|
||||
resp := c.orchestrator.UpdateConfig(configBody.Version, configBody.Config)
|
||||
bdy, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = respWriter.Write(bdy)
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *HTTP2Connection) close() {
|
||||
// Wait for all serve HTTP handlers to return
|
||||
c.activeRequestsWG.Wait()
|
||||
@@ -157,14 +191,16 @@ type http2RespWriter struct {
|
||||
w http.ResponseWriter
|
||||
flusher http.Flusher
|
||||
shouldFlush bool
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (*http2RespWriter, error) {
|
||||
func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type, log *zerolog.Logger) (*http2RespWriter, error) {
|
||||
flusher, isFlusher := w.(http.Flusher)
|
||||
if !isFlusher {
|
||||
respWriter := &http2RespWriter{
|
||||
r: r.Body,
|
||||
w: w,
|
||||
r: r.Body,
|
||||
w: w,
|
||||
log: log,
|
||||
}
|
||||
respWriter.WriteErrorResponse()
|
||||
return nil, fmt.Errorf("%T doesn't implement http.Flusher", w)
|
||||
@@ -175,6 +211,7 @@ func NewHTTP2RespWriter(r *http.Request, w http.ResponseWriter, connType Type) (
|
||||
w: w,
|
||||
flusher: flusher,
|
||||
shouldFlush: connType.shouldFlush(),
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -233,7 +270,7 @@ func (rp *http2RespWriter) Write(p []byte) (n int, err error) {
|
||||
// Implementer of OriginClient should make sure it doesn't write to the connection after Proxy returns
|
||||
// Register a recover routine just in case.
|
||||
if r := recover(); r != nil {
|
||||
println(fmt.Sprintf("Recover from http2 response writer panic, error %s", debug.Stack()))
|
||||
rp.log.Debug().Msgf("Recover from http2 response writer panic, error %s", debug.Stack())
|
||||
}
|
||||
}()
|
||||
n, err = rp.w.Write(p)
|
||||
@@ -249,6 +286,8 @@ func (rp *http2RespWriter) Close() error {
|
||||
|
||||
func determineHTTP2Type(r *http.Request) Type {
|
||||
switch {
|
||||
case isConfigurationUpdate(r):
|
||||
return TypeConfiguration
|
||||
case isWebsocketUpgrade(r):
|
||||
return TypeWebsocket
|
||||
case IsTCPStream(r):
|
||||
@@ -282,6 +321,10 @@ func isWebsocketUpgrade(r *http.Request) bool {
|
||||
return r.Header.Get(InternalUpgradeHeader) == WebsocketUpgrade
|
||||
}
|
||||
|
||||
func isConfigurationUpdate(r *http.Request) bool {
|
||||
return r.Header.Get(InternalUpgradeHeader) == ConfigurationUpdate
|
||||
}
|
||||
|
||||
// IsTCPStream discerns if the connection request needs a tcp stream proxy.
|
||||
func IsTCPStream(r *http.Request) bool {
|
||||
return r.Header.Get(InternalTCPProxySrcHeader) != ""
|
||||
|
||||
+130
-19
@@ -1,7 +1,9 @@
|
||||
package connection
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -27,22 +29,23 @@ var (
|
||||
)
|
||||
|
||||
func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
|
||||
edgeConn, originConn := net.Pipe()
|
||||
edgeConn, cfdConn := net.Pipe()
|
||||
var connIndex = uint8(0)
|
||||
log := zerolog.Nop()
|
||||
obs := NewObserver(&log, &log, false)
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
&NamedTunnelProperties{},
|
||||
connIndex,
|
||||
nil,
|
||||
nil,
|
||||
1*time.Second,
|
||||
)
|
||||
return NewHTTP2Connection(
|
||||
originConn,
|
||||
testConfig,
|
||||
cfdConn,
|
||||
// OriginProxy is set in testConfigManager
|
||||
testOrchestrator,
|
||||
&pogs.ConnectionOptions{},
|
||||
obs,
|
||||
connIndex,
|
||||
@@ -51,6 +54,41 @@ func newTestHTTP2Connection() (*HTTP2Connection, net.Conn) {
|
||||
), edgeConn
|
||||
}
|
||||
|
||||
func TestHTTP2ConfigurationSet(t *testing.T) {
|
||||
http2Conn, edgeConn := newTestHTTP2Connection()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
edgeHTTP2Conn, err := testTransport.NewClientConn(edgeConn)
|
||||
require.NoError(t, err)
|
||||
|
||||
endpoint := fmt.Sprintf("http://localhost:8080/ok")
|
||||
reqBody := []byte(`{
|
||||
"version": 2,
|
||||
"config": {"warp-routing": {"enabled": true}, "originRequest" : {"connectTimeout": 10}, "ingress" : [ {"hostname": "test", "service": "https://localhost:8000" } , {"service": "http_status:404"} ]}}
|
||||
`)
|
||||
reader := bytes.NewReader(reqBody)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, endpoint, reader)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(InternalUpgradeHeader, ConfigurationUpdate)
|
||||
|
||||
resp, err := edgeHTTP2Conn.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
bdy, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, `{"lastAppliedVersion":2,"err":null}`, string(bdy))
|
||||
cancel()
|
||||
wg.Wait()
|
||||
|
||||
}
|
||||
|
||||
func TestServeHTTP(t *testing.T) {
|
||||
tests := []testRequest{
|
||||
{
|
||||
@@ -130,7 +168,7 @@ type mockNamedTunnelRPCClient struct {
|
||||
|
||||
func (mc mockNamedTunnelRPCClient) RegisterConnection(
|
||||
c context.Context,
|
||||
config *NamedTunnelConfig,
|
||||
properties *NamedTunnelProperties,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
@@ -166,6 +204,8 @@ type wsRespWriter struct {
|
||||
*httptest.ResponseRecorder
|
||||
readPipe *io.PipeReader
|
||||
writePipe *io.PipeWriter
|
||||
closed bool
|
||||
panicked bool
|
||||
}
|
||||
|
||||
func newWSRespWriter() *wsRespWriter {
|
||||
@@ -174,46 +214,59 @@ func newWSRespWriter() *wsRespWriter {
|
||||
httptest.NewRecorder(),
|
||||
readPipe,
|
||||
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 {
|
||||
return nowriter{w.readPipe}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (w *wsRespWriter) close() {
|
||||
w.closed = true
|
||||
}
|
||||
|
||||
func TestServeWS(t *testing.T) {
|
||||
http2Conn, _ := newTestHTTP2Connection()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
http2Conn.Serve(ctx)
|
||||
}()
|
||||
|
||||
respWriter := newWSRespWriter()
|
||||
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)
|
||||
req.Header.Set(InternalUpgradeHeader, WebsocketUpgrade)
|
||||
|
||||
wg.Add(1)
|
||||
serveDone := make(chan struct{})
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer close(serveDone)
|
||||
http2Conn.ServeHTTP(respWriter, req)
|
||||
respWriter.close()
|
||||
}()
|
||||
|
||||
data := []byte("test websocket")
|
||||
err = wsutil.WriteClientText(writePipe, data)
|
||||
err = wsutil.WriteClientBinary(writePipe, data)
|
||||
require.NoError(t, err)
|
||||
|
||||
respBody, err := wsutil.ReadServerText(respWriter.RespBody())
|
||||
respBody, err := wsutil.ReadServerBinary(respWriter.RespBody())
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, data, respBody, fmt.Sprintf("Expect %s, got %s", string(data), string(respBody)))
|
||||
|
||||
@@ -223,7 +276,65 @@ func TestServeWS(t *testing.T) {
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
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()
|
||||
cancel()
|
||||
<-serverDone
|
||||
}
|
||||
|
||||
func TestServeControlStream(t *testing.T) {
|
||||
@@ -238,7 +349,7 @@ func TestServeControlStream(t *testing.T) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
&NamedTunnelProperties{},
|
||||
1,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
nil,
|
||||
@@ -288,7 +399,7 @@ func TestFailRegistration(t *testing.T) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
&NamedTunnelProperties{},
|
||||
http2Conn.connIndex,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
nil,
|
||||
@@ -334,7 +445,7 @@ func TestGracefulShutdownHTTP2(t *testing.T) {
|
||||
controlStream := NewControlStream(
|
||||
obs,
|
||||
mockConnectedFuse{},
|
||||
&NamedTunnelConfig{},
|
||||
&NamedTunnelProperties{},
|
||||
http2Conn.connIndex,
|
||||
rpcClientFactory.newMockRPCClient,
|
||||
shutdownC,
|
||||
|
||||
+26
-28
@@ -19,9 +19,7 @@ const (
|
||||
edgeH2TLSServerName = "h2.cftunnel.com"
|
||||
// edgeQUICServerName is the server name to establish quic connection with edge.
|
||||
edgeQUICServerName = "quic.cftunnel.com"
|
||||
// threshold to switch back to h2mux when the user intentionally pick --protocol http2
|
||||
explicitHTTP2FallbackThreshold = -1
|
||||
autoSelectFlag = "auto"
|
||||
autoSelectFlag = "auto"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -197,7 +195,7 @@ type PercentageFetcher func() (edgediscovery.ProtocolPercents, error)
|
||||
func NewProtocolSelector(
|
||||
protocolFlag string,
|
||||
warpRoutingEnabled bool,
|
||||
namedTunnel *NamedTunnelConfig,
|
||||
namedTunnel *NamedTunnelProperties,
|
||||
fetchFunc PercentageFetcher,
|
||||
ttl time.Duration,
|
||||
log *zerolog.Logger,
|
||||
@@ -237,25 +235,23 @@ func selectNamedTunnelProtocols(
|
||||
threshold int32,
|
||||
protocol Protocol,
|
||||
) (ProtocolSelector, error) {
|
||||
if protocolFlag == H2mux.String() {
|
||||
return &staticProtocolSelector{
|
||||
current: H2mux,
|
||||
}, nil
|
||||
// If the user picks a protocol, then we stick to it no matter what.
|
||||
switch protocolFlag {
|
||||
case H2mux.String():
|
||||
return &staticProtocolSelector{current: H2mux}, nil
|
||||
case QUIC.String():
|
||||
return &staticProtocolSelector{current: QUIC}, nil
|
||||
case HTTP2.String():
|
||||
return &staticProtocolSelector{current: HTTP2}, nil
|
||||
}
|
||||
|
||||
if protocolFlag == QUIC.String() {
|
||||
return newAutoProtocolSelector(QUIC, []Protocol{QUIC, HTTP2, H2mux}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
// If the user does not pick (hopefully the majority) then we use the one derived from the TXT DNS record and
|
||||
// fallback on failures.
|
||||
if protocolFlag == autoSelectFlag {
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUIC, HTTP2, H2mux}, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag == HTTP2.String() {
|
||||
return newAutoProtocolSelector(HTTP2, []Protocol{HTTP2, H2mux}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag != autoSelectFlag {
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUIC, HTTP2, H2mux}, threshold, fetchFunc, ttl, log), nil
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
|
||||
func selectWarpRoutingProtocols(
|
||||
@@ -266,19 +262,21 @@ func selectWarpRoutingProtocols(
|
||||
threshold int32,
|
||||
protocol Protocol,
|
||||
) (ProtocolSelector, error) {
|
||||
if protocolFlag == QUIC.String() {
|
||||
return newAutoProtocolSelector(QUICWarp, []Protocol{QUICWarp, HTTP2Warp}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
// If the user picks a protocol, then we stick to it no matter what.
|
||||
switch protocolFlag {
|
||||
case QUIC.String():
|
||||
return &staticProtocolSelector{current: QUICWarp}, nil
|
||||
case HTTP2.String():
|
||||
return &staticProtocolSelector{current: HTTP2Warp}, nil
|
||||
}
|
||||
|
||||
if protocolFlag == HTTP2.String() {
|
||||
return newAutoProtocolSelector(HTTP2Warp, []Protocol{HTTP2Warp}, explicitHTTP2FallbackThreshold, fetchFunc, ttl, log), nil
|
||||
// If the user does not pick (hopefully the majority) then we use the one derived from the TXT DNS record and
|
||||
// fallback on failures.
|
||||
if protocolFlag == autoSelectFlag {
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUICWarp, HTTP2Warp}, threshold, fetchFunc, ttl, log), nil
|
||||
}
|
||||
|
||||
if protocolFlag != autoSelectFlag {
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
|
||||
return newAutoProtocolSelector(protocol, []Protocol{QUICWarp, HTTP2Warp}, threshold, fetchFunc, ttl, log), nil
|
||||
return nil, fmt.Errorf("Unknown protocol %s, %s", protocolFlag, AvailableProtocolFlagMessage)
|
||||
}
|
||||
|
||||
func switchThreshold(accountTag string) int32 {
|
||||
|
||||
+42
-29
@@ -16,7 +16,7 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
testNamedTunnelConfig = &NamedTunnelConfig{
|
||||
testNamedTunnelProperties = &NamedTunnelProperties{
|
||||
Credentials: Credentials{
|
||||
AccountTag: "testAccountTag",
|
||||
},
|
||||
@@ -51,7 +51,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback bool
|
||||
expectedFallback Protocol
|
||||
warpRoutingEnabled bool
|
||||
namedTunnelConfig *NamedTunnelConfig
|
||||
namedTunnelConfig *NamedTunnelProperties
|
||||
fetchFunc PercentageFetcher
|
||||
wantErr bool
|
||||
}{
|
||||
@@ -66,47 +66,59 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
protocol: "h2mux",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: func() (edgediscovery.ProtocolPercents, error) { return nil, nil },
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel over http2",
|
||||
protocol: "http2",
|
||||
expectedProtocol: HTTP2,
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel http2 disabled",
|
||||
name: "named tunnel http2 disabled still gets http2 because it is manually picked",
|
||||
protocol: "http2",
|
||||
expectedProtocol: H2mux,
|
||||
expectedProtocol: HTTP2,
|
||||
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",
|
||||
protocol: "quic",
|
||||
expectedProtocol: QUIC,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel quic and http2 disabled",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel quic disabled",
|
||||
protocol: "quic",
|
||||
protocol: "auto",
|
||||
expectedProtocol: HTTP2,
|
||||
// Hasfallback true is because if http2 fails, then we further fallback to h2mux.
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
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",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to h2mux",
|
||||
protocol: "auto",
|
||||
expectedProtocol: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to http2",
|
||||
@@ -115,7 +127,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: true,
|
||||
expectedFallback: H2mux,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "named tunnel auto to quic",
|
||||
@@ -124,7 +136,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux",
|
||||
@@ -133,7 +145,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing requesting h2mux picks HTTP2 even if http2 percent is -1",
|
||||
@@ -142,7 +154,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing http2",
|
||||
@@ -151,17 +163,17 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing quic",
|
||||
protocol: "quic",
|
||||
protocol: "auto",
|
||||
expectedProtocol: QUICWarp,
|
||||
hasFallback: true,
|
||||
expectedFallback: HTTP2Warp,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto",
|
||||
@@ -170,7 +182,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
hasFallback: false,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
name: "warp routing auto- quic",
|
||||
@@ -180,7 +192,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
expectedFallback: HTTP2Warp,
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}, edgediscovery.ProtocolPercent{Protocol: "quic", Percentage: 100}),
|
||||
warpRoutingEnabled: true,
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
},
|
||||
{
|
||||
// None named tunnel can only use h2mux, so specifying an unknown protocol is not an error
|
||||
@@ -192,14 +204,14 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
name: "named tunnel unknown protocol",
|
||||
protocol: "unknown",
|
||||
fetchFunc: mockFetcher(false, edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "named tunnel fetch error",
|
||||
protocol: "auto",
|
||||
fetchFunc: mockFetcher(true),
|
||||
namedTunnelConfig: testNamedTunnelConfig,
|
||||
namedTunnelConfig: testNamedTunnelProperties,
|
||||
expectedProtocol: HTTP2,
|
||||
wantErr: false,
|
||||
},
|
||||
@@ -225,7 +237,7 @@ func TestNewProtocolSelector(t *testing.T) {
|
||||
|
||||
func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
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.Equal(t, H2mux, selector.Current())
|
||||
|
||||
@@ -254,7 +266,8 @@ func TestAutoProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelConfig, fetcher.fetch(), testNoTTL, &log)
|
||||
// Since the user chooses http2 on purpose, we always stick to it.
|
||||
selector, err := NewProtocolSelector("http2", noWarpRoutingEnabled, testNamedTunnelProperties, fetcher.fetch(), testNoTTL, &log)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
@@ -269,7 +282,7 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}}
|
||||
fetcher.err = nil
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 0}}
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
@@ -278,13 +291,13 @@ func TestHTTP2ProtocolSelectorRefresh(t *testing.T) {
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
|
||||
fetcher.protocolPercents = edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: -1}}
|
||||
assert.Equal(t, H2mux, selector.Current())
|
||||
assert.Equal(t, HTTP2, selector.Current())
|
||||
}
|
||||
|
||||
func TestProtocolSelectorRefreshTTL(t *testing.T) {
|
||||
fetcher := dynamicMockFetcher{}
|
||||
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.Equal(t, QUIC, selector.Current())
|
||||
|
||||
|
||||
+67
-30
@@ -34,78 +34,106 @@ const (
|
||||
|
||||
// QUICConnection represents the type that facilitates Proxying via QUIC streams.
|
||||
type QUICConnection struct {
|
||||
session quic.Session
|
||||
logger *zerolog.Logger
|
||||
httpProxy OriginProxy
|
||||
sessionManager datagramsession.Manager
|
||||
session quic.Session
|
||||
logger *zerolog.Logger
|
||||
orchestrator Orchestrator
|
||||
sessionManager datagramsession.Manager
|
||||
controlStreamHandler ControlStreamHandler
|
||||
connOptions *tunnelpogs.ConnectionOptions
|
||||
}
|
||||
|
||||
// NewQUICConnection returns a new instance of QUICConnection.
|
||||
func NewQUICConnection(
|
||||
ctx context.Context,
|
||||
quicConfig *quic.Config,
|
||||
edgeAddr net.Addr,
|
||||
tlsConfig *tls.Config,
|
||||
httpProxy OriginProxy,
|
||||
orchestrator Orchestrator,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler ControlStreamHandler,
|
||||
observer *Observer,
|
||||
logger *zerolog.Logger,
|
||||
) (*QUICConnection, error) {
|
||||
session, err := quic.DialAddr(edgeAddr.String(), tlsConfig, quicConfig)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial to edge: %w", err)
|
||||
}
|
||||
|
||||
registrationStream, err := session.OpenStream()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open a registration stream: %w", err)
|
||||
}
|
||||
|
||||
err = controlStreamHandler.ServeControlStream(ctx, registrationStream, connOptions, false)
|
||||
if err != nil {
|
||||
// Not wrapping error here to be consistent with the http2 message.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
datagramMuxer, err := quicpogs.NewDatagramMuxer(session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sessionManager := datagramsession.NewManager(datagramMuxer, observer.log)
|
||||
sessionManager := datagramsession.NewManager(datagramMuxer, logger)
|
||||
|
||||
return &QUICConnection{
|
||||
session: session,
|
||||
httpProxy: httpProxy,
|
||||
logger: observer.log,
|
||||
sessionManager: sessionManager,
|
||||
session: session,
|
||||
orchestrator: orchestrator,
|
||||
logger: logger,
|
||||
sessionManager: sessionManager,
|
||||
controlStreamHandler: controlStreamHandler,
|
||||
connOptions: connOptions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Serve starts a QUIC session that begins accepting streams.
|
||||
func (q *QUICConnection) Serve(ctx context.Context) error {
|
||||
// origintunneld assumes the first stream is used for the control plane
|
||||
controlStream, err := q.session.OpenStream()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open a registration control stream: %w", err)
|
||||
}
|
||||
|
||||
// If either goroutine returns nil error, we rely on this cancellation to make sure the other goroutine exits
|
||||
// as fast as possible as well. Nil error means we want to exit for good (caller code won't retry serving this
|
||||
// connection).
|
||||
// If either goroutine returns a non nil error, then the error group cancels the context, thus also canceling the
|
||||
// other goroutine as fast as possible.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
|
||||
// In the future, if cloudflared can autonomously push traffic to the edge, we have to make sure the control
|
||||
// stream is already fully registered before the other goroutines can proceed.
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.serveControlStream(ctx, controlStream)
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.acceptStream(ctx)
|
||||
})
|
||||
errGroup.Go(func() error {
|
||||
defer cancel()
|
||||
return q.sessionManager.Serve(ctx)
|
||||
})
|
||||
|
||||
return errGroup.Wait()
|
||||
}
|
||||
|
||||
func (q *QUICConnection) serveControlStream(ctx context.Context, controlStream quic.Stream) error {
|
||||
// This blocks until the control plane is done.
|
||||
err := q.controlStreamHandler.ServeControlStream(ctx, controlStream, q.connOptions)
|
||||
if err != nil {
|
||||
// Not wrapping error here to be consistent with the http2 message.
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (q *QUICConnection) acceptStream(ctx context.Context) error {
|
||||
defer q.Close()
|
||||
for {
|
||||
stream, err := q.session.AcceptStream(ctx)
|
||||
quicStream, err := q.session.AcceptStream(ctx)
|
||||
if err != nil {
|
||||
// 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) {
|
||||
if errors.Is(err, context.Canceled) || q.controlStreamHandler.IsStopped() {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to accept QUIC stream: %w", err)
|
||||
}
|
||||
go func() {
|
||||
stream := quicpogs.NewSafeStreamCloser(quicStream)
|
||||
defer stream.Close()
|
||||
|
||||
if err = q.handleStream(stream); err != nil {
|
||||
q.logger.Err(err).Msg("Failed to handle QUIC stream")
|
||||
}
|
||||
@@ -118,7 +146,7 @@ func (q *QUICConnection) Close() {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -147,6 +175,10 @@ func (q *QUICConnection) handleDataStream(stream *quicpogs.RequestServerStream)
|
||||
return err
|
||||
}
|
||||
|
||||
originProxy, err := q.orchestrator.GetOriginProxy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch connectRequest.Type {
|
||||
case quicpogs.ConnectionTypeHTTP, quicpogs.ConnectionTypeWebsocket:
|
||||
req, err := buildHTTPRequest(connectRequest, stream)
|
||||
@@ -155,16 +187,16 @@ func (q *QUICConnection) handleDataStream(stream *quicpogs.RequestServerStream)
|
||||
}
|
||||
|
||||
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:
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
@@ -232,6 +264,11 @@ func (q *QUICConnection) UnregisterUdpSession(ctx context.Context, sessionID uui
|
||||
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
|
||||
// the client.
|
||||
type streamReadWriteAcker struct {
|
||||
@@ -305,7 +342,7 @@ func buildHTTPRequest(connectRequest *quicpogs.ConnectRequest, body io.ReadClose
|
||||
// * there is no transfer-encoding=chunked already set.
|
||||
// So, if transfer cannot be chunked and content length is 0, we dont set a request body.
|
||||
if !isWebsocket && !isTransferEncodingChunked(req) && req.ContentLength == 0 {
|
||||
req.Body = nil
|
||||
req.Body = http.NoBody
|
||||
}
|
||||
stripWebsocketUpgradeHeader(req)
|
||||
return req, err
|
||||
|
||||
+29
-55
@@ -3,14 +3,9 @@ package connection
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -33,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
testTLSServerConfig = generateTLSConfig()
|
||||
testTLSServerConfig = quicpogs.GenerateTLSConfig()
|
||||
testQUICConfig = &quic.Config{
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
@@ -52,7 +47,7 @@ func TestQUICServer(t *testing.T) {
|
||||
|
||||
// This is simply a sample websocket frame message.
|
||||
wsBuf := &bytes.Buffer{}
|
||||
wsutil.WriteClientText(wsBuf, []byte("Hello"))
|
||||
wsutil.WriteClientBinary(wsBuf, []byte("Hello"))
|
||||
|
||||
var tests = []struct {
|
||||
desc string
|
||||
@@ -84,7 +79,7 @@ func TestQUICServer(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "test http body request streaming",
|
||||
dest: "/echo_body",
|
||||
dest: "/slow_echo_body",
|
||||
connectionType: quicpogs.ConnectionTypeHTTP,
|
||||
metadata: []quicpogs.Metadata{
|
||||
{
|
||||
@@ -109,7 +104,7 @@ func TestQUICServer(t *testing.T) {
|
||||
},
|
||||
{
|
||||
desc: "test ws proxy",
|
||||
dest: "/ok",
|
||||
dest: "/ws/echo",
|
||||
connectionType: quicpogs.ConnectionTypeWebsocket,
|
||||
metadata: []quicpogs.Metadata{
|
||||
{
|
||||
@@ -130,7 +125,7 @@ func TestQUICServer(t *testing.T) {
|
||||
},
|
||||
},
|
||||
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",
|
||||
@@ -154,7 +149,7 @@ func TestQUICServer(t *testing.T) {
|
||||
)
|
||||
}()
|
||||
|
||||
qc := testQUICConnection(ctx, udpListener.LocalAddr(), t)
|
||||
qc := testQUICConnection(udpListener.LocalAddr(), t)
|
||||
go qc.Serve(ctx)
|
||||
|
||||
wg.Wait()
|
||||
@@ -167,7 +162,8 @@ type fakeControlStream struct {
|
||||
ControlStreamHandler
|
||||
}
|
||||
|
||||
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions, shouldWaitForUnregister bool) error {
|
||||
func (fakeControlStream) ServeControlStream(ctx context.Context, rw io.ReadWriteCloser, connOptions *tunnelpogs.ConnectionOptions) error {
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
func (fakeControlStream) IsStopped() bool {
|
||||
@@ -194,8 +190,9 @@ func quicServer(
|
||||
session, err := earlyListener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
stream, err := session.OpenStreamSync(context.Background())
|
||||
quicStream, err := session.OpenStreamSync(context.Background())
|
||||
require.NoError(t, err)
|
||||
stream := quicpogs.NewSafeStreamCloser(quicStream)
|
||||
|
||||
reqClientStream := quicpogs.RequestClientStream{ReadWriteCloser: stream}
|
||||
err = reqClientStream.WriteConnectRequestData(dest, connectionType, metadata...)
|
||||
@@ -206,42 +203,20 @@ func quicServer(
|
||||
|
||||
if message != nil {
|
||||
// ALPN successful. Write data.
|
||||
_, err := stream.Write([]byte(message))
|
||||
_, err := stream.Write(message)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
response := make([]byte, len(expectedResponse))
|
||||
stream.Read(response)
|
||||
require.NoError(t, err)
|
||||
_, err = stream.Read(response)
|
||||
if err != io.EOF {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// For now it is an echo server. Verify if the same data is returned.
|
||||
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{}
|
||||
|
||||
func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Request, isWebsocket bool) error {
|
||||
@@ -258,11 +233,14 @@ func (moc *mockOriginProxyWithRequest) ProxyHTTP(w ResponseWriter, r *http.Reque
|
||||
}
|
||||
|
||||
if isWebsocket {
|
||||
return wsEndpoint(w, r)
|
||||
return wsEchoEndpoint(w, r)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/ok":
|
||||
originRespEndpoint(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
|
||||
case "/slow_echo_body":
|
||||
time.Sleep(5)
|
||||
fallthrough
|
||||
case "/echo_body":
|
||||
resp := &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
@@ -367,7 +345,7 @@ func TestBuildHTTPRequest(t *testing.T) {
|
||||
},
|
||||
ContentLength: 0,
|
||||
Host: "cf.host",
|
||||
Body: nil,
|
||||
Body: http.NoBody,
|
||||
},
|
||||
body: io.NopCloser(&bytes.Buffer{}),
|
||||
},
|
||||
@@ -532,7 +510,7 @@ func TestServeUDPSession(t *testing.T) {
|
||||
edgeQUICSessionChan <- edgeQUICSession
|
||||
}()
|
||||
|
||||
qc := testQUICConnection(ctx, udpListener.LocalAddr(), t)
|
||||
qc := testQUICConnection(udpListener.LocalAddr(), t)
|
||||
go qc.Serve(ctx)
|
||||
|
||||
edgeQUICSession := <-edgeQUICSessionChan
|
||||
@@ -559,8 +537,7 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
|
||||
}()
|
||||
|
||||
// Send a message to the quic session on edge side, it should be deumx to this datagram session
|
||||
muxedPayload, err := quicpogs.SuffixSessionID(sessionID, payload)
|
||||
require.NoError(t, err)
|
||||
muxedPayload := append(payload, sessionID[:]...)
|
||||
err = edgeQUICSession.SendMessage(muxedPayload)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -583,12 +560,12 @@ func serveSession(ctx context.Context, qc *QUICConnection, edgeQUICSession quic.
|
||||
if closeType != closedByRemote {
|
||||
// Session was not closed by remote, so closeUDPSession should be invoked to unregister from remote
|
||||
unregisterFromEdgeChan := make(chan struct{})
|
||||
rpcServer := &mockSessionRPCServer{
|
||||
sessionRPCServer := &mockSessionRPCServer{
|
||||
sessionID: sessionID,
|
||||
unregisterReason: expectedReason,
|
||||
calledUnregisterChan: unregisterFromEdgeChan,
|
||||
}
|
||||
go runMockSessionRPCServer(ctx, edgeQUICSession, rpcServer, t)
|
||||
go runRPCServer(ctx, edgeQUICSession, sessionRPCServer, nil, t)
|
||||
|
||||
<-unregisterFromEdgeChan
|
||||
}
|
||||
@@ -604,7 +581,7 @@ const (
|
||||
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)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -619,7 +596,7 @@ func runMockSessionRPCServer(ctx context.Context, session quic.Session, rpcServe
|
||||
assert.NoError(t, err)
|
||||
|
||||
log := zerolog.New(os.Stdout)
|
||||
err = rpcServerStream.Serve(rpcServer, &log)
|
||||
err = rpcServerStream.Serve(sessionRPCServer, configRPCServer, &log)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -641,27 +618,24 @@ func (s mockSessionRPCServer) UnregisterUdpSession(ctx context.Context, sessionI
|
||||
return fmt.Errorf("expect unregister reason %s, got %s", s.unregisterReason, reason)
|
||||
}
|
||||
close(s.calledUnregisterChan)
|
||||
fmt.Println("unregister from edge")
|
||||
return nil
|
||||
}
|
||||
|
||||
func testQUICConnection(ctx context.Context, udpListenerAddr net.Addr, t *testing.T) *QUICConnection {
|
||||
func testQUICConnection(udpListenerAddr net.Addr, t *testing.T) *QUICConnection {
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
// Start a mock httpProxy
|
||||
originProxy := &mockOriginProxyWithRequest{}
|
||||
log := zerolog.New(os.Stdout)
|
||||
qc, err := NewQUICConnection(
|
||||
ctx,
|
||||
testQUICConfig,
|
||||
udpListenerAddr,
|
||||
tlsClientConfig,
|
||||
originProxy,
|
||||
&mockOrchestrator{originProxy: &mockOriginProxyWithRequest{}},
|
||||
&tunnelpogs.ConnectionOptions{},
|
||||
fakeControlStream{},
|
||||
NewObserver(&log, &log, false),
|
||||
&log,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
return qc
|
||||
|
||||
+12
-12
@@ -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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -54,7 +54,7 @@ func (tsc *tunnelServerClient) Close() {
|
||||
type NamedTunnelRPCClient interface {
|
||||
RegisterConnection(
|
||||
c context.Context,
|
||||
config *NamedTunnelConfig,
|
||||
config *NamedTunnelProperties,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
@@ -86,15 +86,15 @@ func newRegistrationRPCClient(
|
||||
|
||||
func (rsc *registrationServerClient) RegisterConnection(
|
||||
ctx context.Context,
|
||||
config *NamedTunnelConfig,
|
||||
properties *NamedTunnelProperties,
|
||||
options *tunnelpogs.ConnectionOptions,
|
||||
connIndex uint8,
|
||||
observer *Observer,
|
||||
) error {
|
||||
conn, err := rsc.client.RegisterConnection(
|
||||
ctx,
|
||||
config.Credentials.Auth(),
|
||||
config.Credentials.TunnelID,
|
||||
properties.Credentials.Auth(),
|
||||
properties.Credentials.TunnelID,
|
||||
connIndex,
|
||||
options,
|
||||
)
|
||||
@@ -137,7 +137,7 @@ const (
|
||||
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)
|
||||
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
@@ -174,7 +174,7 @@ type CredentialManager interface {
|
||||
func (h *h2muxConnection) processRegistrationSuccess(
|
||||
registration *tunnelpogs.TunnelRegistration,
|
||||
name rpcName,
|
||||
credentialManager CredentialManager, classicTunnel *ClassicTunnelConfig,
|
||||
credentialManager CredentialManager, classicTunnel *ClassicTunnelProperties,
|
||||
) error {
|
||||
for _, logLine := range registration.LogLines {
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -264,7 +264,7 @@ func (h *h2muxConnection) logServerInfo(ctx context.Context, rpcClient *tunnelSe
|
||||
|
||||
func (h *h2muxConnection) registerNamedTunnel(
|
||||
ctx context.Context,
|
||||
namedTunnel *NamedTunnelConfig,
|
||||
namedTunnel *NamedTunnelProperties,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
) error {
|
||||
stream, err := h.newRPCStream(ctx, register)
|
||||
@@ -283,7 +283,7 @@ func (h *h2muxConnection) registerNamedTunnel(
|
||||
func (h *h2muxConnection) unregister(isNamedTunnel bool) {
|
||||
h.observer.sendUnregisteringEvent(h.connIndex)
|
||||
|
||||
unregisterCtx, cancel := context.WithTimeout(context.Background(), h.config.GracePeriod)
|
||||
unregisterCtx, cancel := context.WithTimeout(context.Background(), h.gracePeriod)
|
||||
defer cancel()
|
||||
|
||||
stream, err := h.newRPCStream(unregisterCtx, unregister)
|
||||
@@ -296,13 +296,13 @@ func (h *h2muxConnection) unregister(isNamedTunnel bool) {
|
||||
rpcClient := h.newRPCClientFunc(unregisterCtx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
rpcClient.GracefulShutdown(unregisterCtx, h.config.GracePeriod)
|
||||
rpcClient.GracefulShutdown(unregisterCtx, h.gracePeriod)
|
||||
} else {
|
||||
rpcClient := NewTunnelServerClient(unregisterCtx, stream, h.observer.log)
|
||||
defer rpcClient.Close()
|
||||
|
||||
// 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")
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -50,7 +51,11 @@ func (m *manager) Serve(ctx context.Context) error {
|
||||
for {
|
||||
sessionID, payload, err := m.transport.ReceiveFrom()
|
||||
if err != nil {
|
||||
return err
|
||||
if aerr, ok := err.(*quic.ApplicationError); ok && uint64(aerr.ErrorCode) == uint64(quic.NoError) {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
datagram := &newDatagram{
|
||||
sessionID: sessionID,
|
||||
@@ -69,7 +74,7 @@ func (m *manager) Serve(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return nil
|
||||
case datagram := <-m.datagramChan:
|
||||
m.sendToSession(datagram)
|
||||
case registration := <-m.registrationChan:
|
||||
|
||||
@@ -106,6 +106,7 @@ func (s *Session) waitForCloseCondition(ctx context.Context, closeAfterIdle time
|
||||
func (s *Session) dstToTransport(buffer []byte) error {
|
||||
n, err := s.dstConn.Read(buffer)
|
||||
s.markActive()
|
||||
// https://pkg.go.dev/io#Reader suggests caller should always process n > 0 bytes
|
||||
if n > 0 {
|
||||
if n <= int(s.transport.MTU()) {
|
||||
err = s.transport.SendTo(s.ID, buffer[:n])
|
||||
@@ -114,10 +115,14 @@ func (s *Session) dstToTransport(buffer []byte) error {
|
||||
s.log.Debug().
|
||||
Str("session", s.ID.String()).
|
||||
Int("len", n).
|
||||
Uint("mtu", s.transport.MTU()).
|
||||
Int("mtu", s.transport.MTU()).
|
||||
Msg("dropped packet exceeding MTU")
|
||||
}
|
||||
}
|
||||
// Some UDP application might send 0-size payload.
|
||||
if err == nil && n == 0 {
|
||||
err = s.transport.SendTo(s.ID, []byte{})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -195,3 +195,40 @@ func TestMarkActiveNotBlocking(t *testing.T) {
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestZeroBytePayload(t *testing.T) {
|
||||
sessionID := uuid.New()
|
||||
cfdConn, originConn := net.Pipe()
|
||||
transport := &mockQUICTransport{
|
||||
reqChan: newDatagramChannel(1),
|
||||
respChan: newDatagramChannel(1),
|
||||
}
|
||||
log := zerolog.Nop()
|
||||
session := newSession(sessionID, transport, cfdConn, &log)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errGroup, ctx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
// Read from underlying conn and send to transport
|
||||
closedByRemote, err := session.Serve(ctx, time.Minute*2)
|
||||
require.Equal(t, context.Canceled, err)
|
||||
require.False(t, closedByRemote)
|
||||
return nil
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
// Write to underlying connection
|
||||
n, err := originConn.Write([]byte{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 0, n)
|
||||
return nil
|
||||
})
|
||||
|
||||
receivedSessionID, payload, err := transport.respChan.Receive(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, payload, 0)
|
||||
require.Equal(t, sessionID, receivedSessionID)
|
||||
|
||||
cancel()
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ type transport interface {
|
||||
// ReceiveFrom reads the next datagram from the transport
|
||||
ReceiveFrom() (uuid.UUID, []byte, error)
|
||||
// Max transmission unit to receive from the transport
|
||||
MTU() uint
|
||||
MTU() int
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ func (mt *mockQUICTransport) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||
return mt.reqChan.Receive(context.Background())
|
||||
}
|
||||
|
||||
func (mt *mockQUICTransport) MTU() uint {
|
||||
return 1217
|
||||
func (mt *mockQUICTransport) MTU() int {
|
||||
return 1280
|
||||
}
|
||||
|
||||
func (mt *mockQUICTransport) newRequest(ctx context.Context, sessionID uuid.UUID, payload []byte) error {
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.16.4 as builder
|
||||
FROM golang:1.17.5 as builder
|
||||
ENV GO111MODULE=on \
|
||||
CGO_ENABLED=0
|
||||
WORKDIR /go/src/github.com/cloudflare/cloudflared/
|
||||
|
||||
@@ -6,8 +6,7 @@ require (
|
||||
github.com/certifi/gocertifi v0.0.0-20200211180108-c7c1fbc02894 // indirect
|
||||
github.com/cloudflare/brotli-go v0.0.0-20191101163834-d34379f7ff93
|
||||
github.com/cloudflare/golibs v0.0.0-20170913112048-333127dbecfc
|
||||
github.com/coredns/coredns v1.7.0
|
||||
github.com/coreos/go-oidc v0.0.0-20171002155002-a93f71fdfe73
|
||||
github.com/coredns/coredns v1.8.7
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
|
||||
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 // indirect
|
||||
@@ -22,52 +21,51 @@ require (
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.0.4
|
||||
github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.4.2
|
||||
github.com/json-iterator/go v1.1.10
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/lucas-clemente/quic-go v0.24.0
|
||||
github.com/mattn/go-colorable v0.1.8
|
||||
github.com/miekg/dns v1.1.31
|
||||
github.com/miekg/dns v1.1.45
|
||||
github.com/mitchellh/go-homedir v1.1.0
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/prometheus/client_model v0.2.0
|
||||
github.com/prometheus/common v0.13.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92
|
||||
github.com/rs/zerolog v1.20.0
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/urfave/cli/v2 v2.2.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/urfave/cli/v2 v2.3.0
|
||||
go.uber.org/automaxprocs v1.4.0
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
golang.org/x/net v0.0.0-20211109214657-ef0fda0de508
|
||||
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
|
||||
golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d
|
||||
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf
|
||||
google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d // indirect
|
||||
google.golang.org/grpc v1.32.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/coreos/go-oidc.v2 v2.1.0
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e
|
||||
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb // indirect
|
||||
google.golang.org/grpc v1.43.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/square/go-jose.v2 v2.4.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
zombiezen.com/go/capnproto2 v2.18.0+incompatible
|
||||
)
|
||||
|
||||
require gopkg.in/coreos/go-oidc.v2 v2.2.1
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
github.com/apparentlymart/go-cidr v1.1.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/caddyserver/caddy v1.0.5 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/coredns/caddy v1.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
@@ -78,23 +76,26 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.4 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.0 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.8 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/onsi/ginkgo v1.16.4 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/procfs v0.1.3 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rivo/uniseg v0.1.0 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
golang.org/x/tools v0.1.1 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.6 // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
)
|
||||
|
||||
replace github.com/urfave/cli/v2 => github.com/ipostelnik/cli/v2 v2.3.1-0.20210324024421-b6ea8234fe3d
|
||||
|
||||
replace github.com/lucas-clemente/quic-go => github.com/chungthuang/quic-go v0.24.1-0.20220110095058-981dc498cb62
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package ingress
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -38,6 +39,34 @@ const (
|
||||
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 {
|
||||
var connectTimeout time.Duration = defaultConnectTimeout
|
||||
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{
|
||||
ConnectTimeout: defaultConnectTimeout,
|
||||
TLSTimeout: defaultTLSTimeout,
|
||||
@@ -128,50 +157,58 @@ func originRequestFromYAML(y config.OriginRequestConfig) OriginRequestConfig {
|
||||
KeepAliveTimeout: defaultKeepAliveTimeout,
|
||||
ProxyAddress: defaultProxyAddress,
|
||||
}
|
||||
if y.ConnectTimeout != nil {
|
||||
out.ConnectTimeout = *y.ConnectTimeout
|
||||
if c.ConnectTimeout != nil {
|
||||
out.ConnectTimeout = c.ConnectTimeout.Duration
|
||||
}
|
||||
if y.TLSTimeout != nil {
|
||||
out.TLSTimeout = *y.TLSTimeout
|
||||
if c.TLSTimeout != nil {
|
||||
out.TLSTimeout = c.TLSTimeout.Duration
|
||||
}
|
||||
if y.TCPKeepAlive != nil {
|
||||
out.TCPKeepAlive = *y.TCPKeepAlive
|
||||
if c.TCPKeepAlive != nil {
|
||||
out.TCPKeepAlive = c.TCPKeepAlive.Duration
|
||||
}
|
||||
if y.NoHappyEyeballs != nil {
|
||||
out.NoHappyEyeballs = *y.NoHappyEyeballs
|
||||
if c.NoHappyEyeballs != nil {
|
||||
out.NoHappyEyeballs = *c.NoHappyEyeballs
|
||||
}
|
||||
if y.KeepAliveConnections != nil {
|
||||
out.KeepAliveConnections = *y.KeepAliveConnections
|
||||
if c.KeepAliveConnections != nil {
|
||||
out.KeepAliveConnections = *c.KeepAliveConnections
|
||||
}
|
||||
if y.KeepAliveTimeout != nil {
|
||||
out.KeepAliveTimeout = *y.KeepAliveTimeout
|
||||
if c.KeepAliveTimeout != nil {
|
||||
out.KeepAliveTimeout = c.KeepAliveTimeout.Duration
|
||||
}
|
||||
if y.HTTPHostHeader != nil {
|
||||
out.HTTPHostHeader = *y.HTTPHostHeader
|
||||
if c.HTTPHostHeader != nil {
|
||||
out.HTTPHostHeader = *c.HTTPHostHeader
|
||||
}
|
||||
if y.OriginServerName != nil {
|
||||
out.OriginServerName = *y.OriginServerName
|
||||
if c.OriginServerName != nil {
|
||||
out.OriginServerName = *c.OriginServerName
|
||||
}
|
||||
if y.CAPool != nil {
|
||||
out.CAPool = *y.CAPool
|
||||
if c.CAPool != nil {
|
||||
out.CAPool = *c.CAPool
|
||||
}
|
||||
if y.NoTLSVerify != nil {
|
||||
out.NoTLSVerify = *y.NoTLSVerify
|
||||
if c.NoTLSVerify != nil {
|
||||
out.NoTLSVerify = *c.NoTLSVerify
|
||||
}
|
||||
if y.DisableChunkedEncoding != nil {
|
||||
out.DisableChunkedEncoding = *y.DisableChunkedEncoding
|
||||
if c.DisableChunkedEncoding != nil {
|
||||
out.DisableChunkedEncoding = *c.DisableChunkedEncoding
|
||||
}
|
||||
if y.BastionMode != nil {
|
||||
out.BastionMode = *y.BastionMode
|
||||
if c.BastionMode != nil {
|
||||
out.BastionMode = *c.BastionMode
|
||||
}
|
||||
if y.ProxyAddress != nil {
|
||||
out.ProxyAddress = *y.ProxyAddress
|
||||
if c.ProxyAddress != nil {
|
||||
out.ProxyAddress = *c.ProxyAddress
|
||||
}
|
||||
if y.ProxyPort != nil {
|
||||
out.ProxyPort = *y.ProxyPort
|
||||
if c.ProxyPort != nil {
|
||||
out.ProxyPort = *c.ProxyPort
|
||||
}
|
||||
if y.ProxyType != nil {
|
||||
out.ProxyType = *y.ProxyType
|
||||
if c.ProxyType != nil {
|
||||
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
|
||||
}
|
||||
@@ -188,10 +225,10 @@ type OriginRequestConfig struct {
|
||||
TCPKeepAlive time.Duration `yaml:"tcpKeepAlive"`
|
||||
// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
|
||||
NoHappyEyeballs bool `yaml:"noHappyEyeballs"`
|
||||
// HTTP proxy maximum keepalive connection pool size
|
||||
KeepAliveConnections int `yaml:"keepAliveConnections"`
|
||||
// HTTP proxy timeout for closing an idle connection
|
||||
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.
|
||||
HTTPHostHeader string `yaml:"httpHostHeader"`
|
||||
// Hostname on the origin server certificate.
|
||||
@@ -220,13 +257,13 @@ type OriginRequestConfig struct {
|
||||
|
||||
func (defaults *OriginRequestConfig) setConnectTimeout(overrides config.OriginRequestConfig) {
|
||||
if val := overrides.ConnectTimeout; val != nil {
|
||||
defaults.ConnectTimeout = *val
|
||||
defaults.ConnectTimeout = val.Duration
|
||||
}
|
||||
}
|
||||
|
||||
func (defaults *OriginRequestConfig) setTLSTimeout(overrides config.OriginRequestConfig) {
|
||||
if val := overrides.TLSTimeout; val != nil {
|
||||
defaults.TLSTimeout = *val
|
||||
defaults.TLSTimeout = val.Duration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,13 +281,13 @@ func (defaults *OriginRequestConfig) setKeepAliveConnections(overrides config.Or
|
||||
|
||||
func (defaults *OriginRequestConfig) setKeepAliveTimeout(overrides config.OriginRequestConfig) {
|
||||
if val := overrides.KeepAliveTimeout; val != nil {
|
||||
defaults.KeepAliveTimeout = *val
|
||||
defaults.KeepAliveTimeout = val.Duration
|
||||
}
|
||||
}
|
||||
|
||||
func (defaults *OriginRequestConfig) setTCPKeepAlive(overrides config.OriginRequestConfig) {
|
||||
if val := overrides.TCPKeepAlive; val != nil {
|
||||
defaults.TCPKeepAlive = *val
|
||||
defaults.TCPKeepAlive = val.Duration
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// Each field has a setter method which sets a value for the field by trying to find:
|
||||
// 1. The user config for this rule
|
||||
@@ -332,5 +382,6 @@ func setConfig(defaults OriginRequestConfig, overrides config.OriginRequestConfi
|
||||
cfg.setProxyPort(overrides)
|
||||
cfg.setProxyAddress(overrides)
|
||||
cfg.setProxyType(overrides)
|
||||
cfg.setIPRules(overrides)
|
||||
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": 60,
|
||||
"tlsTimeout": 1,
|
||||
"noHappyEyeballs": true,
|
||||
"tcpKeepAlive": 1,
|
||||
"keepAliveConnections": 1,
|
||||
"keepAliveTimeout": 1,
|
||||
"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": 120,
|
||||
"tlsTimeout": 2,
|
||||
"noHappyEyeballs": false,
|
||||
"tcpKeepAlive": 2,
|
||||
"keepAliveConnections": 2,
|
||||
"keepAliveTimeout": 2,
|
||||
"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": 120,
|
||||
"tlsTimeout": 2,
|
||||
"noHappyEyeballs": false,
|
||||
"tcpKeepAlive": 2,
|
||||
"keepAliveConnections": 2,
|
||||
"keepAliveTimeout": 2,
|
||||
"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
|
||||
}
|
||||
+8
-8
@@ -7,7 +7,6 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
@@ -127,7 +126,7 @@ func parseSingleOriginService(c *cli.Context, allowURLFromArgs bool) (OriginServ
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Error validating --unix-socket")
|
||||
}
|
||||
return &unixSocketPath{path: path}, nil
|
||||
return &unixSocketPath{path: path, scheme: "http"}, nil
|
||||
}
|
||||
u, err := url.Parse("http://localhost:8080")
|
||||
return &httpService{url: u}, err
|
||||
@@ -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.
|
||||
func (ing Ingress) StartOrigins(
|
||||
wg *sync.WaitGroup,
|
||||
log *zerolog.Logger,
|
||||
shutdownC <-chan struct{},
|
||||
errC chan error,
|
||||
) error {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -163,7 +160,7 @@ func (ing Ingress) CatchAll() *Rule {
|
||||
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))
|
||||
for i, r := range ingress {
|
||||
cfg := setConfig(defaults, r.OriginRequest)
|
||||
@@ -172,7 +169,10 @@ func validate(ingress []config.UnvalidatedIngressRule, defaults OriginRequestCon
|
||||
if prefix := "unix:"; strings.HasPrefix(r.Service, prefix) {
|
||||
// No validation necessary for unix socket filepath services
|
||||
path := strings.TrimPrefix(r.Service, prefix)
|
||||
service = &unixSocketPath{path: path}
|
||||
service = &unixSocketPath{path: path, scheme: "http"}
|
||||
} else if prefix := "unix+tls:"; strings.HasPrefix(r.Service, prefix) {
|
||||
path := strings.TrimPrefix(r.Service, prefix)
|
||||
service = &unixSocketPath{path: path, scheme: "https"}
|
||||
} else if prefix := "http_status:"; strings.HasPrefix(r.Service, prefix) {
|
||||
status, err := strconv.Atoi(strings.TrimPrefix(r.Service, prefix))
|
||||
if err != nil {
|
||||
@@ -290,7 +290,7 @@ func ParseIngress(conf *config.Configuration) (Ingress, error) {
|
||||
if len(conf.Ingress) == 0 {
|
||||
return Ingress{}, ErrNoIngressRules
|
||||
}
|
||||
return validate(conf.Ingress, originRequestFromYAML(conf.OriginRequest))
|
||||
return validateIngress(conf.Ingress, originRequestFromConfig(conf.OriginRequest))
|
||||
}
|
||||
|
||||
func isHTTPService(url *url.URL) bool {
|
||||
|
||||
+32
-5
@@ -26,15 +26,28 @@ ingress:
|
||||
`
|
||||
ing, err := ParseIngress(MustReadIngress(rawYAML))
|
||||
require.NoError(t, err)
|
||||
_, ok := ing.Rules[0].Service.(*unixSocketPath)
|
||||
s, ok := ing.Rules[0].Service.(*unixSocketPath)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "http", s.scheme)
|
||||
}
|
||||
|
||||
func TestParseUnixSocketTLS(t *testing.T) {
|
||||
rawYAML := `
|
||||
ingress:
|
||||
- service: unix+tls:/tmp/echo.sock
|
||||
`
|
||||
ing, err := ParseIngress(MustReadIngress(rawYAML))
|
||||
require.NoError(t, err)
|
||||
s, ok := ing.Rules[0].Service.(*unixSocketPath)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "https", s.scheme)
|
||||
}
|
||||
|
||||
func Test_parseIngress(t *testing.T) {
|
||||
localhost8000 := MustParseURL(t, "https://localhost:8000")
|
||||
localhost8001 := MustParseURL(t, "https://localhost:8001")
|
||||
fourOhFour := newStatusCode(404)
|
||||
defaultConfig := setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{})
|
||||
defaultConfig := setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{})
|
||||
require.Equal(t, defaultKeepAliveConnections, defaultConfig.KeepAliveConnections)
|
||||
tr := true
|
||||
type args struct {
|
||||
@@ -324,7 +337,17 @@ ingress:
|
||||
{
|
||||
Hostname: "socks.foo.com",
|
||||
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,
|
||||
@@ -345,7 +368,7 @@ ingress:
|
||||
{
|
||||
Hostname: "bastion.foo.com",
|
||||
Service: newBastionService(),
|
||||
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
|
||||
Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
|
||||
},
|
||||
{
|
||||
Service: &fourOhFour,
|
||||
@@ -365,7 +388,7 @@ ingress:
|
||||
{
|
||||
Hostname: "bastion.foo.com",
|
||||
Service: newBastionService(),
|
||||
Config: setConfig(originRequestFromYAML(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
|
||||
Config: setConfig(originRequestFromConfig(config.OriginRequestConfig{}), config.OriginRequestConfig{BastionMode: &tr}),
|
||||
},
|
||||
{
|
||||
Service: &fourOhFour,
|
||||
@@ -397,6 +420,10 @@ ingress:
|
||||
}
|
||||
}
|
||||
|
||||
func ipRulePrefix(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func TestSingleOriginSetsConfig(t *testing.T) {
|
||||
flagSet := flag.NewFlagSet(t.Name(), flag.PanicOnError)
|
||||
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)
|
||||
cancel()
|
||||
// Makes sure wsConn stops sending ping before terminating the stream
|
||||
wsConn.WaitForShutdown()
|
||||
wsConn.Close()
|
||||
}
|
||||
|
||||
func (wc *tcpOverWSConnection) Close() {
|
||||
@@ -73,21 +73,8 @@ func (sp *socksProxyOverWSConnection) Stream(ctx context.Context, tunnelConn io.
|
||||
socks.StreamNetHandler(wsConn, sp.accessPolicy, log)
|
||||
cancel()
|
||||
// Makes sure wsConn stops sending ping before terminating the stream
|
||||
wsConn.WaitForShutdown()
|
||||
wsConn.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) {
|
||||
req.URL.Scheme = o.scheme
|
||||
return o.transport.RoundTrip(req)
|
||||
}
|
||||
|
||||
@@ -40,6 +41,8 @@ func (o *httpService) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
|
||||
if o.hostHeader != "" {
|
||||
// For incoming requests, the Host header is promoted to the Request.Host field and removed from the Header map.
|
||||
// Pass the original Host header as X-Forwarded-Host.
|
||||
req.Header.Set("X-Forwarded-Host", req.Host)
|
||||
req.Host = o.hostHeader
|
||||
}
|
||||
return o.transport.RoundTrip(req)
|
||||
|
||||
@@ -3,11 +3,11 @@ package ingress
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -118,7 +118,9 @@ func TestHTTPServiceHostHeaderOverride(t *testing.T) {
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
return
|
||||
}
|
||||
w.Write([]byte("ok"))
|
||||
// return the X-Forwarded-Host header for assertions
|
||||
// as the httptest Server URL isn't available here yet
|
||||
w.Write([]byte(r.Header.Get("X-Forwarded-Host")))
|
||||
}
|
||||
origin := httptest.NewServer(http.HandlerFunc(handler))
|
||||
defer origin.Close()
|
||||
@@ -129,10 +131,8 @@ func TestHTTPServiceHostHeaderOverride(t *testing.T) {
|
||||
httpService := &httpService{
|
||||
url: originURL,
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
shutdownC := make(chan struct{})
|
||||
errC := make(chan error)
|
||||
require.NoError(t, httpService.start(&wg, testLogger, shutdownC, errC, cfg))
|
||||
require.NoError(t, httpService.start(testLogger, shutdownC, cfg))
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, originURL.String(), nil)
|
||||
require.NoError(t, err)
|
||||
@@ -141,6 +141,49 @@ func TestHTTPServiceHostHeaderOverride(t *testing.T) {
|
||||
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(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{}) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+18
-21
@@ -8,7 +8,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -20,18 +19,24 @@ import (
|
||||
"github.com/cloudflare/cloudflared/tlsconfig"
|
||||
)
|
||||
|
||||
const (
|
||||
HelloWorldService = "Hello World test origin"
|
||||
)
|
||||
|
||||
// OriginService is something a tunnel can proxy traffic to.
|
||||
type OriginService interface {
|
||||
String() string
|
||||
// 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
|
||||
// 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 or HTTPS)
|
||||
type unixSocketPath struct {
|
||||
path string
|
||||
scheme string
|
||||
transport *http.Transport
|
||||
}
|
||||
|
||||
@@ -39,7 +44,7 @@ func (o *unixSocketPath) String() string {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -54,7 +59,7 @@ type httpService struct {
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -78,7 +83,7 @@ func (o *rawTCPService) String() string {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -139,7 +144,7 @@ func (o *tcpOverWSService) String() string {
|
||||
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 {
|
||||
o.streamHandler = socks.StreamHandler
|
||||
} else {
|
||||
@@ -148,7 +153,7 @@ func (o *tcpOverWSService) start(wg *sync.WaitGroup, log *zerolog.Logger, shutdo
|
||||
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
|
||||
}
|
||||
|
||||
@@ -164,18 +169,16 @@ type helloWorld struct {
|
||||
}
|
||||
|
||||
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.
|
||||
func (o *helloWorld) start(
|
||||
wg *sync.WaitGroup,
|
||||
log *zerolog.Logger,
|
||||
shutdownC <-chan struct{},
|
||||
errC chan error,
|
||||
cfg OriginRequestConfig,
|
||||
) 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
|
||||
}
|
||||
|
||||
@@ -183,11 +186,7 @@ func (o *helloWorld) start(
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Cannot start Hello World Server")
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = hello.StartHelloWorldServer(log, helloListener, shutdownC)
|
||||
}()
|
||||
go hello.StartHelloWorldServer(log, helloListener, shutdownC)
|
||||
o.server = helloListener
|
||||
|
||||
o.httpService.url = &url.URL{
|
||||
@@ -218,10 +217,8 @@ func (o *statusCode) String() string {
|
||||
}
|
||||
|
||||
func (o *statusCode) start(
|
||||
wg *sync.WaitGroup,
|
||||
log *zerolog.Logger,
|
||||
shutdownC <-chan struct{},
|
||||
errC chan error,
|
||||
_ <-chan struct{},
|
||||
cfg OriginRequestConfig,
|
||||
) error {
|
||||
return nil
|
||||
@@ -296,6 +293,6 @@ func (mos MockOriginHTTPService) String() string {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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,25 @@
|
||||
package orchestration
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
MetricsNamespace = "cloudflared"
|
||||
MetricsSubsystem = "orchestration"
|
||||
)
|
||||
|
||||
var (
|
||||
configVersion = prometheus.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: MetricsNamespace,
|
||||
Subsystem: MetricsSubsystem,
|
||||
Name: "config_version",
|
||||
Help: "Configuration Version",
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(configVersion)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
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")
|
||||
configVersion.Set(float64(version))
|
||||
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,689 @@
|
||||
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": 90,
|
||||
"noHappyEyeballs": true
|
||||
},
|
||||
"ingress": [
|
||||
{
|
||||
"hostname": "jira.tunnel.org",
|
||||
"path": "^\/login",
|
||||
"service": "http://192.16.19.1:443",
|
||||
"originRequest": {
|
||||
"noTLSVerify": true,
|
||||
"connectTimeout": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"hostname": "jira.tunnel.org",
|
||||
"service": "http://172.32.20.6:80",
|
||||
"originRequest": {
|
||||
"noTLSVerify": true,
|
||||
"connectTimeout": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"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": 90,
|
||||
"noHappyEyeballs": true
|
||||
},
|
||||
"ingress": [
|
||||
{
|
||||
"hostname": "%s",
|
||||
"service": "%s",
|
||||
"originRequest": {
|
||||
"httpHostHeader": "%s",
|
||||
"connectTimeout": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"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()
|
||||
log := zerolog.Nop()
|
||||
respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeHTTP, &log)
|
||||
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)
|
||||
|
||||
log := zerolog.Nop()
|
||||
respWriter, err := connection.NewHTTP2RespWriter(req, w, connection.TypeTCP, &log)
|
||||
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==")
|
||||
|
||||
log := zerolog.Nop()
|
||||
respWriter, err := connection.NewHTTP2RespWriter(req, wsRespReadWriter, connection.TypeWebsocket, &log)
|
||||
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,100 +0,0 @@
|
||||
package origin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
)
|
||||
|
||||
type dynamicMockFetcher struct {
|
||||
protocolPercents edgediscovery.ProtocolPercents
|
||||
err error
|
||||
}
|
||||
|
||||
func (dmf *dynamicMockFetcher) fetch() connection.PercentageFetcher {
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
return dmf.protocolPercents, dmf.err
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForBackoffFallback(t *testing.T) {
|
||||
maxRetries := uint(3)
|
||||
backoff := retry.BackoffHandler{
|
||||
MaxRetries: maxRetries,
|
||||
BaseTime: time.Millisecond * 10,
|
||||
}
|
||||
log := zerolog.Nop()
|
||||
resolveTTL := time.Duration(0)
|
||||
namedTunnel := &connection.NamedTunnelConfig{
|
||||
Credentials: connection.Credentials{
|
||||
AccountTag: "test-account",
|
||||
},
|
||||
}
|
||||
mockFetcher := dynamicMockFetcher{
|
||||
protocolPercents: edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}},
|
||||
}
|
||||
warpRoutingEnabled := false
|
||||
protocolSelector, err := connection.NewProtocolSelector(
|
||||
connection.HTTP2.String(),
|
||||
warpRoutingEnabled,
|
||||
namedTunnel,
|
||||
mockFetcher.fetch(),
|
||||
resolveTTL,
|
||||
&log,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
initProtocol := protocolSelector.Current()
|
||||
assert.Equal(t, connection.HTTP2, initProtocol)
|
||||
|
||||
protocolFallback := &protocolFallback{
|
||||
backoff,
|
||||
initProtocol,
|
||||
false,
|
||||
}
|
||||
|
||||
// Retry #0 and #1. At retry #2, we switch protocol, so the fallback loop has one more retry than this
|
||||
for i := 0; i < int(maxRetries-1); i++ {
|
||||
protocolFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protocolFallback, protocolSelector, false)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, initProtocol, protocolFallback.protocol)
|
||||
}
|
||||
|
||||
// Retry fallback protocol
|
||||
for i := 0; i < int(maxRetries); i++ {
|
||||
protocolFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protocolFallback, protocolSelector, false)
|
||||
assert.True(t, ok)
|
||||
fallback, ok := protocolSelector.Fallback()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, fallback, protocolFallback.protocol)
|
||||
}
|
||||
|
||||
currentGlobalProtocol := protocolSelector.Current()
|
||||
assert.Equal(t, initProtocol, currentGlobalProtocol)
|
||||
|
||||
// No protocol to fallback, return error
|
||||
protocolFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protocolFallback, protocolSelector, false)
|
||||
assert.False(t, ok)
|
||||
|
||||
protocolFallback.reset()
|
||||
protocolFallback.BackoffTimer() // simulate retry
|
||||
ok = selectNextProtocol(&log, protocolFallback, protocolSelector, false)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, initProtocol, protocolFallback.protocol)
|
||||
|
||||
protocolFallback.reset()
|
||||
protocolFallback.BackoffTimer() // simulate retry
|
||||
ok = selectNextProtocol(&log, protocolFallback, protocolSelector, true)
|
||||
// Check that we get a true after the first try itself when this flag is true. This allows us to immediately
|
||||
// switch protocols.
|
||||
assert.True(t, ok)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
@@ -43,14 +43,6 @@ var (
|
||||
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() {
|
||||
@@ -59,7 +51,6 @@ func init() {
|
||||
concurrentRequests,
|
||||
responseByCode,
|
||||
requestErrors,
|
||||
haConnections,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@@ -38,17 +38,22 @@ type Proxy struct {
|
||||
// NewOriginProxy returns a new instance of the Proxy struct.
|
||||
func NewOriginProxy(
|
||||
ingressRules ingress.Ingress,
|
||||
warpRouting *ingress.WarpRoutingService,
|
||||
warpRoutingEnabled bool,
|
||||
tags []tunnelpogs.Tag,
|
||||
log *zerolog.Logger,
|
||||
) *Proxy {
|
||||
return &Proxy{
|
||||
proxy := &Proxy{
|
||||
ingressRules: ingressRules,
|
||||
warpRouting: warpRouting,
|
||||
tags: tags,
|
||||
log: log,
|
||||
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
|
||||
@@ -271,11 +276,20 @@ func (wr *bidirectionalStream) Write(p []byte) (n int, err error) {
|
||||
func (p *Proxy) writeEventStream(w connection.ResponseWriter, respBody io.ReadCloser) {
|
||||
reader := bufio.NewReader(respBody)
|
||||
for {
|
||||
line, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
break
|
||||
line, readErr := reader.ReadBytes('\n')
|
||||
|
||||
// We first try to write whatever we read even if an error occurred
|
||||
// The reason for doing it is to guarantee we really push everything to the eyeball side
|
||||
// before returning
|
||||
if len(line) > 0 {
|
||||
if _, writeErr := w.Write(line); writeErr != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if readErr != nil {
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package origin
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -30,8 +31,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
testTags = []tunnelpogs.Tag{tunnelpogs.Tag{Name: "Name", Value: "value"}}
|
||||
unusedWarpRoutingService = (*ingress.WarpRoutingService)(nil)
|
||||
testTags = []tunnelpogs.Tag{tunnelpogs.Tag{Name: "Name", Value: "value"}}
|
||||
)
|
||||
|
||||
type mockHTTPRespWriter struct {
|
||||
@@ -60,6 +60,11 @@ func (w *mockHTTPRespWriter) Read(data []byte) (int, error) {
|
||||
return 0, fmt.Errorf("mockHTTPRespWriter doesn't implement io.Reader")
|
||||
}
|
||||
|
||||
// respHeaders is a test function to read respHeaders
|
||||
func (w *mockHTTPRespWriter) headers() http.Header {
|
||||
return w.Header()
|
||||
}
|
||||
|
||||
type mockWSRespWriter struct {
|
||||
*mockHTTPRespWriter
|
||||
writeNotification chan []byte
|
||||
@@ -130,16 +135,14 @@ func TestProxySingleOrigin(t *testing.T) {
|
||||
ingressRule, err := ingress.NewSingleOrigin(cliCtx, allowURLFromArgs)
|
||||
require.NoError(t, err)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errC := make(chan error)
|
||||
require.NoError(t, ingressRule.StartOrigins(&wg, &log, ctx.Done(), errC))
|
||||
require.NoError(t, ingressRule.StartOrigins(&log, ctx.Done()))
|
||||
|
||||
proxy := NewOriginProxy(ingressRule, unusedWarpRoutingService, testTags, &log)
|
||||
proxy := NewOriginProxy(ingressRule, false, testTags, &log)
|
||||
t.Run("testProxyHTTP", testProxyHTTP(proxy))
|
||||
t.Run("testProxyWebsocket", testProxyWebsocket(proxy))
|
||||
t.Run("testProxySSE", testProxySSE(proxy))
|
||||
t.Run("testProxySSEAllData", testProxySSEAllData(proxy))
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testProxyHTTP(proxy connection.OriginProxy) func(t *testing.T) {
|
||||
@@ -256,6 +259,21 @@ func testProxySSE(proxy connection.OriginProxy) func(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test to guarantee that we always write the contents downstream even if EOF is reached without
|
||||
// hitting the delimiter
|
||||
func testProxySSEAllData(proxy *Proxy) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
eyeballReader := io.NopCloser(strings.NewReader("data\r\r"))
|
||||
responseWriter := newMockSSERespWriter()
|
||||
|
||||
// responseWriter uses an unbuffered channel, so we call in a different go-routine
|
||||
go proxy.writeEventStream(responseWriter, eyeballReader)
|
||||
|
||||
result := string(<-responseWriter.writeNotification)
|
||||
require.Equal(t, "data\r\r", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyMultipleOrigins(t *testing.T) {
|
||||
api := httptest.NewServer(mockAPI{})
|
||||
defer api.Close()
|
||||
@@ -324,11 +342,9 @@ func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.Unvalidat
|
||||
log := zerolog.Nop()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
errC := make(chan error)
|
||||
var wg sync.WaitGroup
|
||||
require.NoError(t, ingress.StartOrigins(&wg, &log, ctx.Done(), errC))
|
||||
require.NoError(t, ingress.StartOrigins(&log, ctx.Done()))
|
||||
|
||||
proxy := NewOriginProxy(ingress, unusedWarpRoutingService, testTags, &log)
|
||||
proxy := NewOriginProxy(ingress, false, testTags, &log)
|
||||
|
||||
for _, test := range tests {
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
@@ -346,7 +362,6 @@ func runIngressTestScenarios(t *testing.T, unvalidatedIngress []config.Unvalidat
|
||||
}
|
||||
}
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
type mockAPI struct{}
|
||||
@@ -377,7 +392,7 @@ func TestProxyError(t *testing.T) {
|
||||
|
||||
log := zerolog.Nop()
|
||||
|
||||
proxy := NewOriginProxy(ing, unusedWarpRoutingService, testTags, &log)
|
||||
proxy := NewOriginProxy(ing, false, testTags, &log)
|
||||
|
||||
responseWriter := newMockHTTPRespWriter()
|
||||
req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1", nil)
|
||||
@@ -447,7 +462,7 @@ func TestConnections(t *testing.T) {
|
||||
// eyeball connection type.
|
||||
connectionType connection.Type
|
||||
|
||||
//requestheaders to be sent in the call to proxy.Proxy
|
||||
// requestheaders to be sent in the call to proxy.Proxy
|
||||
requestHeaders http.Header
|
||||
}
|
||||
|
||||
@@ -508,7 +523,7 @@ func TestConnections(t *testing.T) {
|
||||
args: args{
|
||||
ingressServiceScheme: "ws://",
|
||||
originService: runEchoWSService,
|
||||
//eyeballResponseWriter gets set after roundtrip dial.
|
||||
// eyeballResponseWriter gets set after roundtrip dial.
|
||||
eyeballRequestBody: newPipedWSRequestBody([]byte("test3")),
|
||||
warpRoutingService: ingress.NewWarpRoutingService(),
|
||||
requestHeaders: map[string][]string{
|
||||
@@ -544,6 +559,24 @@ func TestConnections(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Send (unexpected) HTTP when origin expects WS (to unwrap for raw TCP)
|
||||
name: "http-(ws)tcp proxy",
|
||||
args: args{
|
||||
ingressServiceScheme: "tcp://",
|
||||
originService: runEchoTCPService,
|
||||
eyeballResponseWriter: newMockHTTPRespWriter(),
|
||||
eyeballRequestBody: http.NoBody,
|
||||
connectionType: connection.TypeHTTP,
|
||||
requestHeaders: map[string][]string{
|
||||
"Cf-Cloudflared-Proxy-Src": {"non-blank-value"},
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
message: []byte{},
|
||||
headers: map[string][]string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tcp-tcp proxy without warpRoutingService enabled",
|
||||
args: args{
|
||||
@@ -617,10 +650,9 @@ func TestConnections(t *testing.T) {
|
||||
test.args.originService(t, ln)
|
||||
|
||||
ingressRule := createSingleIngressConfig(t, test.args.ingressServiceScheme+ln.Addr().String())
|
||||
var wg sync.WaitGroup
|
||||
errC := make(chan error)
|
||||
ingressRule.StartOrigins(&wg, logger, ctx.Done(), errC)
|
||||
proxy := NewOriginProxy(ingressRule, test.args.warpRoutingService, testTags, logger)
|
||||
ingressRule.StartOrigins(logger, ctx.Done())
|
||||
proxy := NewOriginProxy(ingressRule, true, testTags, logger)
|
||||
proxy.warpRouting = test.args.warpRoutingService
|
||||
|
||||
dest := ln.Addr().String()
|
||||
req, err := http.NewRequest(
|
||||
@@ -641,8 +673,8 @@ func TestConnections(t *testing.T) {
|
||||
}()
|
||||
}
|
||||
if test.args.connectionType == connection.TypeTCP {
|
||||
rws := connection.NewHTTPResponseReadWriterAcker(respWriter, req)
|
||||
err = proxy.ProxyTCP(ctx, rws, &connection.TCPRequest{Dest: dest})
|
||||
rwa := connection.NewHTTPResponseReadWriterAcker(respWriter, req)
|
||||
err = proxy.ProxyTCP(ctx, rwa, &connection.TCPRequest{Dest: dest})
|
||||
} else {
|
||||
err = proxy.ProxyHTTP(respWriter, req, test.args.connectionType == connection.TypeWebsocket)
|
||||
}
|
||||
+9
-12
@@ -9,10 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// Max datagram frame size is limited to 1220 https://github.com/lucas-clemente/quic-go/blob/v0.24.0/internal/protocol/params.go#L138
|
||||
// However, 3 more bytes are reserved https://github.com/lucas-clemente/quic-go/blob/v0.24.0/internal/wire/datagram_frame.go#L61
|
||||
MaxDatagramFrameSize = 1217
|
||||
sessionIDLen = len(uuid.UUID{})
|
||||
sessionIDLen = len(uuid.UUID{})
|
||||
)
|
||||
|
||||
type DatagramMuxer struct {
|
||||
@@ -34,11 +31,11 @@ func NewDatagramMuxer(quicSession quic.Session) (*DatagramMuxer, error) {
|
||||
// SendTo suffix the session ID to the payload so the other end of the QUIC session can demultiplex
|
||||
// the payload from multiple datagram sessions
|
||||
func (dm *DatagramMuxer) SendTo(sessionID uuid.UUID, payload []byte) error {
|
||||
if len(payload) > MaxDatagramFrameSize-sessionIDLen {
|
||||
if len(payload) > maxDatagramPayloadSize {
|
||||
// TODO: TUN-5302 return ICMP packet too big message
|
||||
return fmt.Errorf("origin UDP payload has %d bytes, which exceeds transport MTU %d", len(payload), dm.MTU())
|
||||
}
|
||||
msgWithID, err := SuffixSessionID(sessionID, payload)
|
||||
msgWithID, err := suffixSessionID(sessionID, payload)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Failed to suffix session ID to datagram, it will be dropped")
|
||||
}
|
||||
@@ -56,17 +53,17 @@ func (dm *DatagramMuxer) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||
if err != nil {
|
||||
return uuid.Nil, nil, err
|
||||
}
|
||||
return ExtractSessionID(msg)
|
||||
return extractSessionID(msg)
|
||||
}
|
||||
|
||||
// Maximum application payload to send to / receive from QUIC datagram frame
|
||||
func (dm *DatagramMuxer) MTU() uint {
|
||||
return uint(MaxDatagramFrameSize - sessionIDLen)
|
||||
func (dm *DatagramMuxer) MTU() int {
|
||||
return maxDatagramPayloadSize
|
||||
}
|
||||
|
||||
// Each QUIC datagram should be suffixed with session ID.
|
||||
// ExtractSessionID extracts the session ID and a slice with only the payload
|
||||
func ExtractSessionID(b []byte) (uuid.UUID, []byte, error) {
|
||||
// extractSessionID extracts the session ID and a slice with only the payload
|
||||
func extractSessionID(b []byte) (uuid.UUID, []byte, error) {
|
||||
msgLen := len(b)
|
||||
if msgLen < sessionIDLen {
|
||||
return uuid.Nil, nil, fmt.Errorf("session ID has %d bytes, but data only has %d", sessionIDLen, len(b))
|
||||
@@ -82,7 +79,7 @@ func ExtractSessionID(b []byte) (uuid.UUID, []byte, error) {
|
||||
|
||||
// SuffixSessionID appends the session ID at the end of the payload. Suffix is more performant than prefix because
|
||||
// the payload slice might already have enough capacity to append the session ID at the end
|
||||
func SuffixSessionID(sessionID uuid.UUID, b []byte) ([]byte, error) {
|
||||
func suffixSessionID(sessionID uuid.UUID, b []byte) ([]byte, error) {
|
||||
if len(b)+len(sessionID) > MaxDatagramFrameSize {
|
||||
return nil, fmt.Errorf("datagram size exceed %d", MaxDatagramFrameSize)
|
||||
}
|
||||
|
||||
+28
-14
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
@@ -23,11 +24,11 @@ var (
|
||||
|
||||
func TestSuffixThenRemoveSessionID(t *testing.T) {
|
||||
msg := []byte(t.Name())
|
||||
msgWithID, err := SuffixSessionID(testSessionID, msg)
|
||||
msgWithID, err := suffixSessionID(testSessionID, msg)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, msgWithID, len(msg)+sessionIDLen)
|
||||
|
||||
sessionID, msgWithoutID, err := ExtractSessionID(msgWithID)
|
||||
sessionID, msgWithoutID, err := extractSessionID(msgWithID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, msg, msgWithoutID)
|
||||
require.Equal(t, testSessionID, sessionID)
|
||||
@@ -36,26 +37,27 @@ func TestSuffixThenRemoveSessionID(t *testing.T) {
|
||||
func TestRemoveSessionIDError(t *testing.T) {
|
||||
// message is too short to contain session ID
|
||||
msg := []byte("test")
|
||||
_, _, err := ExtractSessionID(msg)
|
||||
_, _, err := extractSessionID(msg)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestSuffixSessionIDError(t *testing.T) {
|
||||
msg := make([]byte, MaxDatagramFrameSize-sessionIDLen)
|
||||
_, err := SuffixSessionID(testSessionID, msg)
|
||||
_, err := suffixSessionID(testSessionID, msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
msg = make([]byte, MaxDatagramFrameSize-sessionIDLen+1)
|
||||
_, err = SuffixSessionID(testSessionID, msg)
|
||||
_, err = suffixSessionID(testSessionID, msg)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMaxDatagramPayload(t *testing.T) {
|
||||
payload := make([]byte, MaxDatagramFrameSize-sessionIDLen)
|
||||
payload := make([]byte, maxDatagramPayloadSize)
|
||||
|
||||
quicConfig := &quic.Config{
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
}
|
||||
quicListener := newQUICListener(t, quicConfig)
|
||||
defer quicListener.Close()
|
||||
@@ -65,13 +67,19 @@ func TestMaxDatagramPayload(t *testing.T) {
|
||||
errGroup.Go(func() error {
|
||||
// Accept quic connection
|
||||
quicSession, err := quicListener.Accept(ctx)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
muxer, err := NewDatagramMuxer(quicSession)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionID, receivedPayload, err := muxer.ReceiveFrom()
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
require.Equal(t, testSessionID, sessionID)
|
||||
require.True(t, bytes.Equal(payload, receivedPayload))
|
||||
|
||||
@@ -89,13 +97,19 @@ func TestMaxDatagramPayload(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer, err := NewDatagramMuxer(quicSession)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait a few milliseconds for MTU discovery to take place
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
err = muxer.SendTo(testSessionID, payload)
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Payload larger than transport MTU, should return an error
|
||||
largePayload := append(payload, byte(1))
|
||||
largePayload := make([]byte, MaxDatagramFrameSize)
|
||||
err = muxer.SendTo(testSessionID, largePayload)
|
||||
require.Error(t, err)
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package quic
|
||||
|
||||
const (
|
||||
MaxDatagramFrameSize = 1350
|
||||
// maxDatagramPayloadSize is the maximum packet size allowed by warp client
|
||||
maxDatagramPayloadSize = 1280
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build windows
|
||||
|
||||
package quic
|
||||
|
||||
const (
|
||||
// Due to https://github.com/lucas-clemente/quic-go/issues/3273, MTU discovery is disabled on Windows
|
||||
// 1220 is the default value https://github.com/lucas-clemente/quic-go/blob/84e03e59760ceee37359688871bb0688fcc4e98f/internal/protocol/params.go#L138
|
||||
MaxDatagramFrameSize = 1220
|
||||
// 3 more bytes are reserved at https://github.com/lucas-clemente/quic-go/blob/v0.24.0/internal/wire/datagram_frame.go#L61
|
||||
maxDatagramPayloadSize = MaxDatagramFrameSize - 3 - sessionIDLen
|
||||
)
|
||||
+16
-9
@@ -17,8 +17,8 @@ import (
|
||||
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
|
||||
// not write data before writing the metadata.
|
||||
// ProtocolSignature defines the first 6 bytes of the stream, which is used to distinguish the type of stream. It
|
||||
// ensures whoever performs a handshake does not write data before writing the metadata.
|
||||
type ProtocolSignature [6]byte
|
||||
|
||||
var (
|
||||
@@ -29,12 +29,15 @@ var (
|
||||
RPCStreamProtocolSignature = ProtocolSignature{0x52, 0xBB, 0x82, 0x5C, 0xDB, 0x65}
|
||||
)
|
||||
|
||||
const protocolVersionLength = 2
|
||||
|
||||
type protocolVersion string
|
||||
|
||||
const (
|
||||
protocolV1 protocolVersion = "01"
|
||||
|
||||
protocolVersionLength = 2
|
||||
|
||||
HandshakeIdleTimeout = 5 * time.Second
|
||||
MaxIdleTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
// RequestServerStream is a stream to serve requests
|
||||
@@ -122,7 +125,7 @@ func (rcs *RequestClientStream) ReadConnectResponseData() (*ConnectResponse, err
|
||||
return nil, err
|
||||
}
|
||||
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.
|
||||
@@ -154,13 +157,13 @@ func NewRPCServerStream(stream io.ReadWriteCloser, protocol ProtocolSignature) (
|
||||
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
|
||||
rpcLogger := logger.Level(zerolog.ErrorLevel)
|
||||
rpcTransport := tunnelrpc.NewTransportLogger(&rpcLogger, rpc.StreamTransport(s))
|
||||
defer rpcTransport.Close()
|
||||
|
||||
main := tunnelpogs.SessionManager_ServerToClient(sessionManager)
|
||||
main := tunnelpogs.CloudflaredServer_ServerToClient(sessionManager, configManager)
|
||||
rpcConn := rpc.NewConn(
|
||||
rpcTransport,
|
||||
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
|
||||
type RPCClientStream struct {
|
||||
client tunnelpogs.SessionManager_PogsClient
|
||||
client tunnelpogs.CloudflaredServer_PogsClient
|
||||
transport rpc.Transport
|
||||
}
|
||||
|
||||
@@ -238,7 +241,7 @@ func NewRPCClientStream(ctx context.Context, stream io.ReadWriteCloser, logger *
|
||||
tunnelrpc.ConnLog(logger),
|
||||
)
|
||||
return &RPCClientStream{
|
||||
client: tunnelpogs.SessionManager_PogsClient{Client: conn.Bootstrap(ctx), Conn: conn},
|
||||
client: tunnelpogs.NewCloudflaredServer_PogsClient(conn.Bootstrap(ctx), conn),
|
||||
transport: transport,
|
||||
}, nil
|
||||
}
|
||||
@@ -255,6 +258,10 @@ func (rcs *RPCClientStream) UnregisterUdpSession(ctx context.Context, sessionID
|
||||
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() {
|
||||
_ = rcs.client.Close()
|
||||
_ = rcs.transport.Close()
|
||||
|
||||
+80
-13
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -108,14 +110,10 @@ func TestConnectResponseMeta(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRegisterUdpSession(t *testing.T) {
|
||||
clientReader, serverWriter := io.Pipe()
|
||||
serverReader, clientWriter := io.Pipe()
|
||||
|
||||
clientStream := mockRPCStream{clientReader, clientWriter}
|
||||
serverStream := mockRPCStream{serverReader, serverWriter}
|
||||
clientStream, serverStream := newMockRPCStreams()
|
||||
|
||||
unregisterMessage := "closed by eyeball"
|
||||
rpcServer := mockRPCServer{
|
||||
sessionRPCServer := mockSessionRPCServer{
|
||||
sessionID: uuid.New(),
|
||||
dstIP: net.IP{172, 16, 0, 1},
|
||||
dstPort: 8000,
|
||||
@@ -129,7 +127,7 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
rpcServerStream, err := NewRPCServerStream(serverStream, protocol)
|
||||
assert.NoError(t, err)
|
||||
err = rpcServerStream.Serve(rpcServer, &logger)
|
||||
err = rpcServerStream.Serve(sessionRPCServer, nil, &logger)
|
||||
assert.NoError(t, err)
|
||||
|
||||
serverStream.Close()
|
||||
@@ -139,12 +137,12 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||
rpcClientStream, err := NewRPCClientStream(context.Background(), clientStream, &logger)
|
||||
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
|
||||
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
|
||||
assert.Error(t, rpcClientStream.UnregisterUdpSession(context.Background(), uuid.New(), unregisterMessage))
|
||||
@@ -153,7 +151,48 @@ func TestRegisterUdpSession(t *testing.T) {
|
||||
<-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
|
||||
dstIP net.IP
|
||||
dstPort uint16
|
||||
@@ -161,7 +200,7 @@ type mockRPCServer struct {
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
io.ReadCloser
|
||||
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 {
|
||||
_ = s.ReadCloser.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"},
|
||||
}
|
||||
}
|
||||
+7
-11
@@ -15,10 +15,10 @@ import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
homedir "github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
cfpath "github.com/cloudflare/cloudflared/token"
|
||||
@@ -87,37 +87,33 @@ func SignCert(token, pubKey string) (string, error) {
|
||||
return "", errors.New("invalid token")
|
||||
}
|
||||
|
||||
jwt, err := jose.ParseJWT(token)
|
||||
parsedToken, err := jwt.ParseSigned(token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse JWT")
|
||||
}
|
||||
|
||||
claims, err := jwt.Claims()
|
||||
claims := jwt.Claims{}
|
||||
err = parsedToken.UnsafeClaimsWithoutVerification(&claims)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to retrieve JWT claims")
|
||||
}
|
||||
|
||||
issuer, _, err := claims.StringClaim("iss")
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to retrieve JWT iss")
|
||||
}
|
||||
|
||||
buf, err := json.Marshal(&signPayload{
|
||||
PublicKey: pubKey,
|
||||
JWT: token,
|
||||
Issuer: issuer,
|
||||
Issuer: claims.Issuer,
|
||||
})
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to marshal signPayload")
|
||||
}
|
||||
var res *http.Response
|
||||
if mockRequest != nil {
|
||||
res, err = mockRequest(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
res, err = mockRequest(claims.Issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
} else {
|
||||
client := http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
res, err = client.Post(issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
res, err = client.Post(claims.Issuer+signEndpoint, "application/json", bytes.NewBuffer(buf))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
+19
-17
@@ -4,8 +4,6 @@
|
||||
package sshgen
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -18,8 +16,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-oidc/jose"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/square/go-jose.v2"
|
||||
"gopkg.in/square/go-jose.v2/jwt"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
cfpath "github.com/cloudflare/cloudflared/token"
|
||||
@@ -97,22 +96,25 @@ func TestCertGenSuccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func tokenGenerator() string {
|
||||
iat := time.Now().Unix()
|
||||
exp := time.Now().Add(time.Minute * 5).Unix()
|
||||
claims := jose.Claims{}
|
||||
claims.Add("aud", audTest)
|
||||
claims.Add("iat", iat)
|
||||
claims.Add("nonce", nonceTest)
|
||||
claims.Add("exp", exp)
|
||||
iat := time.Now()
|
||||
exp := time.Now().Add(time.Minute * 5)
|
||||
|
||||
k, err := rsa.GenerateKey(rand.Reader, 512)
|
||||
claims := jwt.Claims{
|
||||
Audience: jwt.Audience{audTest},
|
||||
IssuedAt: jwt.NewNumericDate(iat),
|
||||
Expiry: jwt.NewNumericDate(exp),
|
||||
}
|
||||
|
||||
key := []byte("secret")
|
||||
signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
|
||||
if err != nil {
|
||||
return ""
|
||||
panic(err)
|
||||
}
|
||||
signer := jose.NewSignerRSA("asdf", *k)
|
||||
token, terr := jose.NewSignedJWT(claims, signer)
|
||||
if terr != nil {
|
||||
return ""
|
||||
|
||||
signedToken, err := jwt.Signed(signer).Claims(claims).CompactSerialize()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return token.Encode()
|
||||
|
||||
return signedToken
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"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 (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
tunnelpogs "github.com/cloudflare/cloudflared/tunnelrpc/pogs"
|
||||
@@ -38,6 +39,7 @@ const (
|
||||
type Supervisor struct {
|
||||
cloudflaredUUID uuid.UUID
|
||||
config *TunnelConfig
|
||||
orchestrator *orchestration.Orchestrator
|
||||
edgeIPs *edgediscovery.Edge
|
||||
tunnelErrors chan tunnelError
|
||||
tunnelsConnecting map[int]chan struct{}
|
||||
@@ -64,7 +66,7 @@ type tunnelError struct {
|
||||
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()
|
||||
if err != nil {
|
||||
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{
|
||||
cloudflaredUUID: cloudflaredUUID,
|
||||
config: config,
|
||||
orchestrator: orchestrator,
|
||||
edgeIPs: edgeIPs,
|
||||
tunnelErrors: make(chan tunnelError),
|
||||
tunnelsConnecting: map[int]chan struct{}{},
|
||||
@@ -243,6 +246,7 @@ func (s *Supervisor) startFirstTunnel(
|
||||
ctx,
|
||||
s.reconnectCredentialManager,
|
||||
s.config,
|
||||
s.orchestrator,
|
||||
addr,
|
||||
s.log,
|
||||
firstConnIndex,
|
||||
@@ -277,6 +281,7 @@ func (s *Supervisor) startFirstTunnel(
|
||||
ctx,
|
||||
s.reconnectCredentialManager,
|
||||
s.config,
|
||||
s.orchestrator,
|
||||
addr,
|
||||
s.log,
|
||||
firstConnIndex,
|
||||
@@ -311,6 +316,7 @@ func (s *Supervisor) startTunnel(
|
||||
ctx,
|
||||
s.reconnectCredentialManager,
|
||||
s.config,
|
||||
s.orchestrator,
|
||||
addr,
|
||||
s.log,
|
||||
uint8(index),
|
||||
@@ -380,7 +386,7 @@ func (s *Supervisor) authenticate(ctx context.Context, numPreviousAttempts int)
|
||||
defer rpcClient.Close()
|
||||
|
||||
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)
|
||||
return rpcClient.Authenticate(ctx, s.config.ClassicTunnel, registrationOptions)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery/allregions"
|
||||
"github.com/cloudflare/cloudflared/h2mux"
|
||||
"github.com/cloudflare/cloudflared/orchestration"
|
||||
quicpogs "github.com/cloudflare/cloudflared/quic"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
"github.com/cloudflare/cloudflared/signal"
|
||||
@@ -31,44 +32,37 @@ const (
|
||||
dialTimeout = 15 * time.Second
|
||||
FeatureSerializedHeaders = "serialized_headers"
|
||||
FeatureQuickReconnects = "quick_reconnects"
|
||||
quicHandshakeIdleTimeout = 5 * time.Second
|
||||
quicMaxIdleTimeout = 15 * time.Second
|
||||
)
|
||||
|
||||
type rpcName string
|
||||
|
||||
const (
|
||||
reconnect rpcName = "reconnect"
|
||||
authenticate rpcName = " authenticate"
|
||||
FeatureAllowRemoteConfig = "allow_remote_config"
|
||||
)
|
||||
|
||||
type TunnelConfig struct {
|
||||
ConnectionConfig *connection.Config
|
||||
OSArch string
|
||||
ClientID string
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
EdgeAddrs []string
|
||||
Region string
|
||||
HAConnections int
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
LBPool string
|
||||
Tags []tunnelpogs.Tag
|
||||
Log *zerolog.Logger
|
||||
LogTransport *zerolog.Logger
|
||||
Observer *connection.Observer
|
||||
ReportedVersion string
|
||||
Retries uint
|
||||
RunFromTerminal bool
|
||||
GracePeriod time.Duration
|
||||
ReplaceExisting bool
|
||||
OSArch string
|
||||
ClientID string
|
||||
CloseConnOnce *sync.Once // Used to close connectedSignal no more than once
|
||||
EdgeAddrs []string
|
||||
Region string
|
||||
HAConnections int
|
||||
IncidentLookup IncidentLookup
|
||||
IsAutoupdated bool
|
||||
LBPool string
|
||||
Tags []tunnelpogs.Tag
|
||||
Log *zerolog.Logger
|
||||
LogTransport *zerolog.Logger
|
||||
Observer *connection.Observer
|
||||
ReportedVersion string
|
||||
Retries uint
|
||||
RunFromTerminal bool
|
||||
|
||||
NamedTunnel *connection.NamedTunnelConfig
|
||||
ClassicTunnel *connection.ClassicTunnelConfig
|
||||
NamedTunnel *connection.NamedTunnelProperties
|
||||
ClassicTunnel *connection.ClassicTunnelProperties
|
||||
MuxerConfig *connection.MuxerConfig
|
||||
ProtocolSelector connection.ProtocolSelector
|
||||
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
|
||||
if c.HAConnections <= 1 && c.LBPool == "" {
|
||||
policy = tunnelrpc.ExistingTunnelPolicy_disconnect
|
||||
@@ -90,7 +84,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
|
||||
host, _, _ := net.SplitHostPort(originLocalAddr)
|
||||
originIP := net.ParseIP(host)
|
||||
@@ -98,7 +92,7 @@ func (c *TunnelConfig) ConnectionOptions(originLocalAddr string, numPreviousAtte
|
||||
return &tunnelpogs.ConnectionOptions{
|
||||
Client: c.NamedTunnel.Client,
|
||||
OriginLocalIP: originIP,
|
||||
ReplaceExisting: c.ConnectionConfig.ReplaceExisting,
|
||||
ReplaceExisting: c.ReplaceExisting,
|
||||
CompressionQuality: uint8(c.MuxerConfig.CompressionSetting),
|
||||
NumPreviousAttempts: numPreviousAttempts,
|
||||
}
|
||||
@@ -115,11 +109,12 @@ func (c *TunnelConfig) SupportedFeatures() []string {
|
||||
func StartTunnelDaemon(
|
||||
ctx context.Context,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
connectedSignal *signal.Signal,
|
||||
reconnectCh chan ReconnectSignal,
|
||||
graceShutdownC <-chan struct{},
|
||||
) error {
|
||||
s, err := NewSupervisor(config, reconnectCh, graceShutdownC)
|
||||
s, err := NewSupervisor(config, orchestrator, reconnectCh, graceShutdownC)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -130,6 +125,7 @@ func ServeTunnelLoop(
|
||||
ctx context.Context,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
addr *allregions.EdgeAddr,
|
||||
connAwareLogger *ConnAwareLogger,
|
||||
connIndex uint8,
|
||||
@@ -165,6 +161,7 @@ func ServeTunnelLoop(
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
orchestrator,
|
||||
addr,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
@@ -192,12 +189,11 @@ func ServeTunnelLoop(
|
||||
case <-gracefulShutdownC:
|
||||
return nil
|
||||
case <-protocolFallback.BackoffTimer():
|
||||
var idleTimeoutError *quic.IdleTimeoutError
|
||||
if !selectNextProtocol(
|
||||
connLog.Logger(),
|
||||
protocolFallback,
|
||||
config.ProtocolSelector,
|
||||
errors.As(err, &idleTimeoutError),
|
||||
err,
|
||||
) {
|
||||
return err
|
||||
}
|
||||
@@ -230,9 +226,13 @@ func selectNextProtocol(
|
||||
connLog *zerolog.Logger,
|
||||
protocolBackoff *protocolFallback,
|
||||
selector connection.ProtocolSelector,
|
||||
isNetworkActivityTimeout bool,
|
||||
cause error,
|
||||
) bool {
|
||||
if protocolBackoff.ReachedMaxRetries() || isNetworkActivityTimeout {
|
||||
var idleTimeoutError *quic.IdleTimeoutError
|
||||
isNetworkActivityTimeout := errors.As(cause, &idleTimeoutError)
|
||||
_, hasFallback := selector.Fallback()
|
||||
|
||||
if protocolBackoff.ReachedMaxRetries() || (hasFallback && isNetworkActivityTimeout) {
|
||||
fallback, hasFallback := selector.Fallback()
|
||||
if !hasFallback {
|
||||
return false
|
||||
@@ -260,6 +260,7 @@ func ServeTunnel(
|
||||
connLog *ConnAwareLogger,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
addr *allregions.EdgeAddr,
|
||||
connIndex uint8,
|
||||
fuse *h2mux.BooleanFuse,
|
||||
@@ -288,6 +289,7 @@ func ServeTunnel(
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
orchestrator,
|
||||
addr,
|
||||
connIndex,
|
||||
fuse,
|
||||
@@ -336,6 +338,7 @@ func serveTunnel(
|
||||
connLog *ConnAwareLogger,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
addr *allregions.EdgeAddr,
|
||||
connIndex uint8,
|
||||
fuse *h2mux.BooleanFuse,
|
||||
@@ -345,7 +348,6 @@ func serveTunnel(
|
||||
protocol connection.Protocol,
|
||||
gracefulShutdownC <-chan struct{},
|
||||
) (err error, recoverable bool) {
|
||||
|
||||
connectedFuse := &connectedFuse{
|
||||
fuse: fuse,
|
||||
backoff: backoff,
|
||||
@@ -357,15 +359,16 @@ func serveTunnel(
|
||||
connIndex,
|
||||
nil,
|
||||
gracefulShutdownC,
|
||||
config.ConnectionConfig.GracePeriod,
|
||||
config.GracePeriod,
|
||||
)
|
||||
|
||||
switch protocol {
|
||||
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,
|
||||
addr.UDP,
|
||||
config,
|
||||
orchestrator,
|
||||
connLog,
|
||||
connOptions,
|
||||
controlStream,
|
||||
@@ -380,11 +383,12 @@ func serveTunnel(
|
||||
return err, true
|
||||
}
|
||||
|
||||
connOptions := config.ConnectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries()))
|
||||
connOptions := config.connectionOptions(edgeConn.LocalAddr().String(), uint8(backoff.Retries()))
|
||||
if err := ServeHTTP2(
|
||||
ctx,
|
||||
connLog,
|
||||
config,
|
||||
orchestrator,
|
||||
edgeConn,
|
||||
connOptions,
|
||||
controlStream,
|
||||
@@ -407,6 +411,7 @@ func serveTunnel(
|
||||
connLog,
|
||||
credentialManager,
|
||||
config,
|
||||
orchestrator,
|
||||
edgeConn,
|
||||
connIndex,
|
||||
connectedFuse,
|
||||
@@ -433,6 +438,7 @@ func ServeH2mux(
|
||||
connLog *ConnAwareLogger,
|
||||
credentialManager *reconnectCredentialManager,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
edgeConn net.Conn,
|
||||
connIndex uint8,
|
||||
connectedFuse *connectedFuse,
|
||||
@@ -443,7 +449,8 @@ func ServeH2mux(
|
||||
connLog.Logger().Debug().Msgf("Connecting via h2mux")
|
||||
// Returns error from parsing the origin URL or handshake errors
|
||||
handler, err, recoverable := connection.NewH2muxConnection(
|
||||
config.ConnectionConfig,
|
||||
orchestrator,
|
||||
config.GracePeriod,
|
||||
config.MuxerConfig,
|
||||
edgeConn,
|
||||
connIndex,
|
||||
@@ -461,10 +468,10 @@ func ServeH2mux(
|
||||
|
||||
errGroup.Go(func() error {
|
||||
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)
|
||||
}
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -479,6 +486,7 @@ func ServeHTTP2(
|
||||
ctx context.Context,
|
||||
connLog *ConnAwareLogger,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
tlsServerConn net.Conn,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler connection.ControlStreamHandler,
|
||||
@@ -489,7 +497,7 @@ func ServeHTTP2(
|
||||
connLog.Logger().Debug().Msgf("Connecting via http2")
|
||||
h2conn := connection.NewHTTP2Connection(
|
||||
tlsServerConn,
|
||||
config.ConnectionConfig,
|
||||
orchestrator,
|
||||
connOptions,
|
||||
config.Observer,
|
||||
connIndex,
|
||||
@@ -518,6 +526,7 @@ func ServeQUIC(
|
||||
ctx context.Context,
|
||||
edgeAddr *net.UDPAddr,
|
||||
config *TunnelConfig,
|
||||
orchestrator *orchestration.Orchestrator,
|
||||
connLogger *ConnAwareLogger,
|
||||
connOptions *tunnelpogs.ConnectionOptions,
|
||||
controlStreamHandler connection.ControlStreamHandler,
|
||||
@@ -527,52 +536,48 @@ func ServeQUIC(
|
||||
) (err error, recoverable bool) {
|
||||
tlsConfig := config.EdgeTLSConfigs[connection.QUIC]
|
||||
quicConfig := &quic.Config{
|
||||
HandshakeIdleTimeout: quicHandshakeIdleTimeout,
|
||||
MaxIdleTimeout: quicMaxIdleTimeout,
|
||||
HandshakeIdleTimeout: quicpogs.HandshakeIdleTimeout,
|
||||
MaxIdleTimeout: quicpogs.MaxIdleTimeout,
|
||||
MaxIncomingStreams: connection.MaxConcurrentStreams,
|
||||
MaxIncomingUniStreams: connection.MaxConcurrentStreams,
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: quicpogs.MaxDatagramFrameSize,
|
||||
Tracer: quicpogs.NewClientTracer(connLogger.Logger(), connIndex),
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
quicConn, err := connection.NewQUICConnection(
|
||||
ctx,
|
||||
quicConfig,
|
||||
edgeAddr,
|
||||
tlsConfig,
|
||||
config.ConnectionConfig.OriginProxy,
|
||||
connOptions,
|
||||
controlStreamHandler,
|
||||
config.Observer)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msgf("Failed to create new quic connection")
|
||||
return err, true
|
||||
}
|
||||
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
err := quicConn.Serve(ctx)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msg("Failed to serve quic connection")
|
||||
}
|
||||
return fmt.Errorf("Connection with edge closed")
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
return listenReconnect(serveCtx, reconnectCh, gracefulShutdownC)
|
||||
})
|
||||
|
||||
err = errGroup.Wait()
|
||||
if err == nil {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
quicConn, err := connection.NewQUICConnection(
|
||||
quicConfig,
|
||||
edgeAddr,
|
||||
tlsConfig,
|
||||
orchestrator,
|
||||
connOptions,
|
||||
controlStreamHandler,
|
||||
connLogger.Logger())
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msgf("Failed to create new quic connection")
|
||||
return err, true
|
||||
}
|
||||
|
||||
errGroup, serveCtx := errgroup.WithContext(ctx)
|
||||
errGroup.Go(func() error {
|
||||
err := quicConn.Serve(serveCtx)
|
||||
if err != nil {
|
||||
connLogger.ConnAwareLogger().Err(err).Msg("Failed to serve quic connection")
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
errGroup.Go(func() error {
|
||||
err := listenReconnect(serveCtx, reconnectCh, gracefulShutdownC)
|
||||
if err != nil {
|
||||
// forcefully break the connection (this is only used for testing)
|
||||
quicConn.Close()
|
||||
}
|
||||
return err
|
||||
})
|
||||
|
||||
return errGroup.Wait(), false
|
||||
}
|
||||
|
||||
func listenReconnect(ctx context.Context, reconnectCh <-chan ReconnectSignal, gracefulShutdownCh <-chan struct{}) error {
|
||||
@@ -0,0 +1,120 @@
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/edgediscovery"
|
||||
"github.com/cloudflare/cloudflared/retry"
|
||||
)
|
||||
|
||||
type dynamicMockFetcher struct {
|
||||
protocolPercents edgediscovery.ProtocolPercents
|
||||
err error
|
||||
}
|
||||
|
||||
func (dmf *dynamicMockFetcher) fetch() connection.PercentageFetcher {
|
||||
return func() (edgediscovery.ProtocolPercents, error) {
|
||||
return dmf.protocolPercents, dmf.err
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForBackoffFallback(t *testing.T) {
|
||||
maxRetries := uint(3)
|
||||
backoff := retry.BackoffHandler{
|
||||
MaxRetries: maxRetries,
|
||||
BaseTime: time.Millisecond * 10,
|
||||
}
|
||||
log := zerolog.Nop()
|
||||
resolveTTL := time.Duration(0)
|
||||
namedTunnel := &connection.NamedTunnelProperties{}
|
||||
mockFetcher := dynamicMockFetcher{
|
||||
protocolPercents: edgediscovery.ProtocolPercents{edgediscovery.ProtocolPercent{Protocol: "http2", Percentage: 100}},
|
||||
}
|
||||
warpRoutingEnabled := false
|
||||
protocolSelector, err := connection.NewProtocolSelector(
|
||||
"auto",
|
||||
warpRoutingEnabled,
|
||||
namedTunnel,
|
||||
mockFetcher.fetch(),
|
||||
resolveTTL,
|
||||
&log,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
|
||||
initProtocol := protocolSelector.Current()
|
||||
assert.Equal(t, connection.HTTP2, initProtocol)
|
||||
|
||||
protoFallback := &protocolFallback{
|
||||
backoff,
|
||||
initProtocol,
|
||||
false,
|
||||
}
|
||||
|
||||
// Retry #0 and #1. At retry #2, we switch protocol, so the fallback loop has one more retry than this
|
||||
for i := 0; i < int(maxRetries-1); i++ {
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protoFallback, protocolSelector, nil)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, initProtocol, protoFallback.protocol)
|
||||
}
|
||||
|
||||
// Retry fallback protocol
|
||||
for i := 0; i < int(maxRetries); i++ {
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protoFallback, protocolSelector, nil)
|
||||
assert.True(t, ok)
|
||||
fallback, ok := protocolSelector.Fallback()
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, fallback, protoFallback.protocol)
|
||||
}
|
||||
|
||||
currentGlobalProtocol := protocolSelector.Current()
|
||||
assert.Equal(t, initProtocol, currentGlobalProtocol)
|
||||
|
||||
// No protocol to fallback, return error
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protoFallback, protocolSelector, nil)
|
||||
assert.False(t, ok)
|
||||
|
||||
protoFallback.reset()
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
ok = selectNextProtocol(&log, protoFallback, protocolSelector, nil)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, initProtocol, protoFallback.protocol)
|
||||
|
||||
protoFallback.reset()
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
ok = selectNextProtocol(&log, protoFallback, protocolSelector, &quic.IdleTimeoutError{})
|
||||
// Check that we get a true after the first try itself when this flag is true. This allows us to immediately
|
||||
// switch protocols when there is a fallback.
|
||||
assert.True(t, ok)
|
||||
|
||||
// But if there is no fallback available, then we exhaust the retries despite the type of error.
|
||||
// The reason why there's no fallback available is because we pick a specific protocol instead of letting it be auto.
|
||||
protocolSelector, err = connection.NewProtocolSelector(
|
||||
"quic",
|
||||
warpRoutingEnabled,
|
||||
namedTunnel,
|
||||
mockFetcher.fetch(),
|
||||
resolveTTL,
|
||||
&log,
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
protoFallback = &protocolFallback{backoff, protocolSelector.Current(), false}
|
||||
for i := 0; i < int(maxRetries-1); i++ {
|
||||
protoFallback.BackoffTimer() // simulate retry
|
||||
ok := selectNextProtocol(&log, protoFallback, protocolSelector, &quic.IdleTimeoutError{})
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, connection.QUIC, protoFallback.protocol)
|
||||
}
|
||||
// And finally it fails as it should, with no fallback.
|
||||
protoFallback.BackoffTimer()
|
||||
ok = selectNextProtocol(&log, protoFallback, protocolSelector, &quic.IdleTimeoutError{})
|
||||
assert.False(t, ok)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package origin
|
||||
package supervisor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -19,6 +19,8 @@ type TLSParameters struct {
|
||||
RootCAs []string
|
||||
ServerName string
|
||||
CurvePreferences []tls.CurveID
|
||||
MinVersion uint16 // min tls version. If zero, TLS1.0 is defined as minimum.
|
||||
MaxVersion uint16 // max tls version. If zero, last TLS version is used defined as limit (currently TLS1.3)
|
||||
}
|
||||
|
||||
// GetConfig returns a TLS configuration according to the Config set by the user.
|
||||
@@ -72,6 +74,9 @@ func GetConfig(p *TLSParameters) (*tls.Config, error) {
|
||||
tlsconfig.CurvePreferences = []tls.CurveID{tls.CurveP256}
|
||||
}
|
||||
|
||||
tlsconfig.MinVersion = p.MinVersion
|
||||
tlsconfig.MaxVersion = p.MaxVersion
|
||||
|
||||
return tlsconfig, nil
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user