mirror of
https://github.com/cloudflare/cloudflared
synced 2026-06-08 13:33:07 +00:00
Compare commits
77 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 | |||
| 7c7cf688e6 | |||
| a39d95d5f7 | |||
| 01ad2785ee | |||
| 6822e4f8ab | |||
| 834c0eaeed | |||
| 74a3026963 | |||
| 8445b88d3c | |||
| 7a55208c61 | |||
| 581cfb8480 | |||
| 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
|
||||
|
||||
+46
-3
@@ -1,8 +1,51 @@
|
||||
**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
|
||||
- Fix logging when `quic` transport is used and UDP traffic is proxied.
|
||||
- FIPS compliant cloudflared binaries will now be released as separate artifacts. Recall that these are only for linux
|
||||
and amd64.
|
||||
|
||||
## 2021.12.1
|
||||
### Bug Fixe
|
||||
- Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before).
|
||||
### Bug Fixes
|
||||
- Fixes Github issue #530 where cloudflared 2021.12.0 could not reach origins that were HTTPS and using certain encryption
|
||||
methods forbidden by FIPS compliance (such as Let's Encrypt certificates). To address this fix we have temporarily reverted
|
||||
FIPS compliance from amd64 linux binaries that was recently introduced (or fixed actually as it was never working before).
|
||||
|
||||
## 2021.12.0
|
||||
### New Features
|
||||
|
||||
@@ -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,12 +23,16 @@ 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)
|
||||
LINK_FLAGS := -linkmode=external -extldflags=-static $(LINK_FLAGS)
|
||||
# Prevent linking with libc regardless of CGO enabled or not.
|
||||
GO_BUILD_TAGS := $(GO_BUILD_TAGS) osusergo netgo fips
|
||||
VERSION_FLAGS := $(VERSION_FLAGS) -X "main.BuildType=FIPS"
|
||||
endif
|
||||
|
||||
LDFLAGS := -ldflags='$(VERSION_FLAGS) $(LINK_FLAGS)'
|
||||
@@ -32,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),)
|
||||
@@ -136,32 +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-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,90 @@
|
||||
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
|
||||
- 2021-12-27 TUN-5551: Show whether the binary was built for FIPS compliance
|
||||
|
||||
2021.12.3
|
||||
- 2021-12-22 TUN-5584: Changes for release 2021.12.2
|
||||
- 2021-12-22 TUN-5590: QUIC datagram max user payload is 1217 bytes
|
||||
- 2021-12-22 TUN-5593: Read full packet from UDP connection, even if it exceeds MTU of the transport. When packet length is greater than the MTU of the transport, we will silently drop packets (for now).
|
||||
- 2021-12-23 TUN-5597: Log session ID when session is terminated by edge
|
||||
|
||||
2021.12.2
|
||||
- 2021-12-20 TUN-5571: Remove redundant session manager log, it's already logged in origin/tunnel.ServeQUIC
|
||||
- 2021-12-20 TUN-5570: Only log RPC server events at error level to reduce noise
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultTimeout = 15 * time.Second
|
||||
jsonContentType = "application/json"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrBadRequest = errors.New("incorrect request parameters")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrAPINoSuccess = errors.New("API call failed")
|
||||
)
|
||||
|
||||
type RESTClient struct {
|
||||
baseEndpoints *baseEndpoints
|
||||
authToken string
|
||||
userAgent string
|
||||
client http.Client
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
type baseEndpoints struct {
|
||||
accountLevel url.URL
|
||||
zoneLevel url.URL
|
||||
accountRoutes url.URL
|
||||
accountVnets url.URL
|
||||
}
|
||||
|
||||
var _ Client = (*RESTClient)(nil)
|
||||
|
||||
func NewRESTClient(baseURL, accountTag, zoneTag, authToken, userAgent string, log *zerolog.Logger) (*RESTClient, error) {
|
||||
if strings.HasSuffix(baseURL, "/") {
|
||||
baseURL = baseURL[:len(baseURL)-1]
|
||||
}
|
||||
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")
|
||||
}
|
||||
accountRoutesEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/routes", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create route account-level endpoint")
|
||||
}
|
||||
accountVnetsEndpoint, err := url.Parse(fmt.Sprintf("%s/accounts/%s/teamnet/virtual_networks", baseURL, accountTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create virtual network account-level endpoint")
|
||||
}
|
||||
zoneLevelEndpoint, err := url.Parse(fmt.Sprintf("%s/zones/%s/tunnels", baseURL, zoneTag))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create account level endpoint")
|
||||
}
|
||||
httpTransport := http.Transport{
|
||||
TLSHandshakeTimeout: defaultTimeout,
|
||||
ResponseHeaderTimeout: defaultTimeout,
|
||||
}
|
||||
http2.ConfigureTransport(&httpTransport)
|
||||
return &RESTClient{
|
||||
baseEndpoints: &baseEndpoints{
|
||||
accountLevel: *accountLevelEndpoint,
|
||||
zoneLevel: *zoneLevelEndpoint,
|
||||
accountRoutes: *accountRoutesEndpoint,
|
||||
accountVnets: *accountVnetsEndpoint,
|
||||
},
|
||||
authToken: authToken,
|
||||
userAgent: userAgent,
|
||||
client: http.Client{
|
||||
Transport: &httpTransport,
|
||||
Timeout: defaultTimeout,
|
||||
},
|
||||
log: log,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RESTClient) sendRequest(method string, url url.URL, body interface{}) (*http.Response, error) {
|
||||
var bodyReader io.Reader
|
||||
if body != nil {
|
||||
if bodyBytes, err := json.Marshal(body); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to serialize json body")
|
||||
} else {
|
||||
bodyReader = bytes.NewBuffer(bodyBytes)
|
||||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, url.String(), bodyReader)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "can't create %s request", method)
|
||||
}
|
||||
req.Header.Set("User-Agent", r.userAgent)
|
||||
if bodyReader != nil {
|
||||
req.Header.Set("Content-Type", jsonContentType)
|
||||
}
|
||||
req.Header.Add("X-Auth-User-Service-Key", r.authToken)
|
||||
req.Header.Add("Accept", "application/json;version=1")
|
||||
return r.client.Do(req)
|
||||
}
|
||||
|
||||
func parseResponse(reader io.Reader, data interface{}) error {
|
||||
// Schema for Tunnelstore responses in the v1 API.
|
||||
// Roughly, it's a wrapper around a particular result that adds failures/errors/etc
|
||||
var result response
|
||||
// First, parse the wrapper and check the API call succeeded
|
||||
if err := json.NewDecoder(reader).Decode(&result); err != nil {
|
||||
return errors.Wrap(err, "failed to decode response")
|
||||
}
|
||||
if err := result.checkErrors(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !result.Success {
|
||||
return ErrAPINoSuccess
|
||||
}
|
||||
// At this point we know the API call succeeded, so, parse out the inner
|
||||
// result into the datatype provided as a parameter.
|
||||
if err := json.Unmarshal(result.Result, &data); err != nil {
|
||||
return errors.Wrap(err, "the Cloudflare API response was an unexpected type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type response struct {
|
||||
Success bool `json:"success,omitempty"`
|
||||
Errors []apiErr `json:"errors,omitempty"`
|
||||
Messages []string `json:"messages,omitempty"`
|
||||
Result json.RawMessage `json:"result,omitempty"`
|
||||
}
|
||||
|
||||
func (r *response) checkErrors() error {
|
||||
if len(r.Errors) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(r.Errors) == 1 {
|
||||
return r.Errors[0]
|
||||
}
|
||||
var messages string
|
||||
for _, e := range r.Errors {
|
||||
messages += fmt.Sprintf("%s; ", e)
|
||||
}
|
||||
return fmt.Errorf("API errors: %s", messages)
|
||||
}
|
||||
|
||||
type apiErr struct {
|
||||
Code json.Number `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
func (e apiErr) Error() string {
|
||||
return fmt.Sprintf("code: %v, reason: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
func (r *RESTClient) statusCodeToError(op string, resp *http.Response) error {
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
var errorsResp response
|
||||
if json.NewDecoder(resp.Body).Decode(&errorsResp) == nil {
|
||||
if err := errorsResp.checkErrors(); err != nil {
|
||||
return errors.Errorf("Failed to %s: %s", op, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return nil
|
||||
case http.StatusBadRequest:
|
||||
return ErrBadRequest
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return ErrUnauthorized
|
||||
case http.StatusNotFound:
|
||||
return ErrNotFound
|
||||
}
|
||||
return errors.Errorf("API call to %s failed with status %d: %s", op,
|
||||
resp.StatusCode, http.StatusText(resp.StatusCode))
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type TunnelClient interface {
|
||||
CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error)
|
||||
GetTunnel(tunnelID uuid.UUID) (*Tunnel, error)
|
||||
DeleteTunnel(tunnelID uuid.UUID) error
|
||||
ListTunnels(filter *TunnelFilter) ([]*Tunnel, error)
|
||||
ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error)
|
||||
CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error
|
||||
}
|
||||
|
||||
type HostnameClient interface {
|
||||
RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error)
|
||||
}
|
||||
|
||||
type IPRouteClient interface {
|
||||
ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error)
|
||||
AddRoute(newRoute NewRoute) (Route, error)
|
||||
DeleteRoute(params DeleteRouteParams) error
|
||||
GetByIP(params GetRouteByIpParams) (DetailedRoute, error)
|
||||
}
|
||||
|
||||
type VnetClient interface {
|
||||
CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error)
|
||||
ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error)
|
||||
DeleteVirtualNetwork(id uuid.UUID) error
|
||||
UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
TunnelClient
|
||||
HostnameClient
|
||||
IPRouteClient
|
||||
VnetClient
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Change = string
|
||||
|
||||
const (
|
||||
ChangeNew = "new"
|
||||
ChangeUpdated = "updated"
|
||||
ChangeUnchanged = "unchanged"
|
||||
)
|
||||
|
||||
// HostnameRoute represents a record type that can route to a tunnel
|
||||
type HostnameRoute interface {
|
||||
json.Marshaler
|
||||
RecordType() string
|
||||
UnmarshalResult(body io.Reader) (HostnameRouteResult, error)
|
||||
String() string
|
||||
}
|
||||
|
||||
type HostnameRouteResult interface {
|
||||
// SuccessSummary explains what will route to this tunnel when it's provisioned successfully
|
||||
SuccessSummary() string
|
||||
}
|
||||
|
||||
type DNSRoute struct {
|
||||
userHostname string
|
||||
overwriteExisting bool
|
||||
}
|
||||
|
||||
type DNSRouteResult struct {
|
||||
route *DNSRoute
|
||||
CName Change `json:"cname"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewDNSRoute(userHostname string, overwriteExisting bool) HostnameRoute {
|
||||
return &DNSRoute{
|
||||
userHostname: userHostname,
|
||||
overwriteExisting: overwriteExisting,
|
||||
}
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
UserHostname string `json:"user_hostname"`
|
||||
OverwriteExisting bool `json:"overwrite_existing"`
|
||||
}{
|
||||
Type: dr.RecordType(),
|
||||
UserHostname: dr.userHostname,
|
||||
OverwriteExisting: dr.overwriteExisting,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result DNSRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = dr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) RecordType() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (dr *DNSRoute) String() string {
|
||||
return fmt.Sprintf("%s %s", dr.RecordType(), dr.userHostname)
|
||||
}
|
||||
|
||||
func (res *DNSRouteResult) SuccessSummary() string {
|
||||
var msgFmt string
|
||||
switch res.CName {
|
||||
case ChangeNew:
|
||||
msgFmt = "Added CNAME %s which will route to this tunnel"
|
||||
case ChangeUpdated: // this is not currently returned by tunnelsore
|
||||
msgFmt = "%s updated to route to your tunnel"
|
||||
case ChangeUnchanged:
|
||||
msgFmt = "%s is already configured to route to your tunnel"
|
||||
}
|
||||
return fmt.Sprintf(msgFmt, res.hostname())
|
||||
}
|
||||
|
||||
// hostname yields the resulting name for the DNS route; if that is not available from Cloudflare API, then the
|
||||
// requested name is returned instead (should not be the common path, it is just a fall-back).
|
||||
func (res *DNSRouteResult) hostname() string {
|
||||
if res.Name != "" {
|
||||
return res.Name
|
||||
}
|
||||
return res.route.userHostname
|
||||
}
|
||||
|
||||
type LBRoute struct {
|
||||
lbName string
|
||||
lbPool string
|
||||
}
|
||||
|
||||
type LBRouteResult struct {
|
||||
route *LBRoute
|
||||
LoadBalancer Change `json:"load_balancer"`
|
||||
Pool Change `json:"pool"`
|
||||
}
|
||||
|
||||
func NewLBRoute(lbName, lbPool string) HostnameRoute {
|
||||
return &LBRoute{
|
||||
lbName: lbName,
|
||||
lbPool: lbPool,
|
||||
}
|
||||
}
|
||||
|
||||
func (lr *LBRoute) MarshalJSON() ([]byte, error) {
|
||||
s := struct {
|
||||
Type string `json:"type"`
|
||||
LBName string `json:"lb_name"`
|
||||
LBPool string `json:"lb_pool"`
|
||||
}{
|
||||
Type: lr.RecordType(),
|
||||
LBName: lr.lbName,
|
||||
LBPool: lr.lbPool,
|
||||
}
|
||||
return json.Marshal(&s)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) RecordType() string {
|
||||
return "lb"
|
||||
}
|
||||
|
||||
func (lb *LBRoute) String() string {
|
||||
return fmt.Sprintf("%s %s %s", lb.RecordType(), lb.lbName, lb.lbPool)
|
||||
}
|
||||
|
||||
func (lr *LBRoute) UnmarshalResult(body io.Reader) (HostnameRouteResult, error) {
|
||||
var result LBRouteResult
|
||||
err := parseResponse(body, &result)
|
||||
result.route = lr
|
||||
return &result, err
|
||||
}
|
||||
|
||||
func (res *LBRouteResult) SuccessSummary() string {
|
||||
var msg string
|
||||
switch res.LoadBalancer + "," + res.Pool {
|
||||
case "new,new":
|
||||
msg = "Created load balancer %s and added a new pool %s with this tunnel as an origin"
|
||||
case "new,updated":
|
||||
msg = "Created load balancer %s with an existing pool %s which was updated to use this tunnel as an origin"
|
||||
case "new,unchanged":
|
||||
msg = "Created load balancer %s with an existing pool %s which already has this tunnel as an origin"
|
||||
case "updated,new":
|
||||
msg = "Added new pool %[2]s with this tunnel as an origin to load balancer %[1]s"
|
||||
case "updated,updated":
|
||||
msg = "Updated pool %[2]s to use this tunnel as an origin and added it to load balancer %[1]s"
|
||||
case "updated,unchanged":
|
||||
msg = "Added pool %[2]s, which already has this tunnel as an origin, to load balancer %[1]s"
|
||||
case "unchanged,updated":
|
||||
msg = "Added this tunnel as an origin in pool %[2]s which is already used by load balancer %[1]s"
|
||||
case "unchanged,unchanged":
|
||||
msg = "Load balancer %s already uses pool %s which has this tunnel as an origin"
|
||||
case "unchanged,new":
|
||||
// this state is not possible
|
||||
fallthrough
|
||||
default:
|
||||
msg = "Something went wrong: failed to modify load balancer %s with pool %s; please check traffic manager configuration in the dashboard"
|
||||
}
|
||||
|
||||
return fmt.Sprintf(msg, res.route.lbName, res.route.lbPool)
|
||||
}
|
||||
|
||||
func (r *RESTClient) RouteTunnel(tunnelID uuid.UUID, route HostnameRoute) (HostnameRouteResult, error) {
|
||||
endpoint := r.baseEndpoints.zoneLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/routes", tunnelID))
|
||||
resp, err := r.sendRequest("PUT", endpoint, route)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return route.UnmarshalResult(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
@@ -1,17 +1,9 @@
|
||||
package tunnelstore
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -105,125 +97,3 @@ func TestLBRouteResultSuccessSummary(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, actual, "case %d", i+1)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
reader io.Reader
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
// TODO: Add test cases.
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := unmarshalTunnel(tt.args.reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
Name: "test",
|
||||
CreatedAt: time.Time{},
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalConnections(t *testing.T) {
|
||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||
expected := ActiveClient{
|
||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||
Features: []string{"ha-origin"},
|
||||
Version: "2021.2.5",
|
||||
Arch: "darwin_amd64",
|
||||
RunAt: time.Time{},
|
||||
Connections: []Connection{{
|
||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||
ColoName: "LIS",
|
||||
IsPendingReconnect: false,
|
||||
OriginIP: net.ParseIP("148.38.28.2"),
|
||||
OpenedAt: time.Time{},
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
@@ -1,9 +1,13 @@
|
||||
package teamnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -133,3 +137,104 @@ type GetRouteByIpParams struct {
|
||||
// Optional field. If unset, backend will assume the default vnet for the account.
|
||||
VNetID *uuid.UUID
|
||||
}
|
||||
|
||||
// ListRoutes calls the Tunnelstore GET endpoint for all routes under an account.
|
||||
func (r *RESTClient) ListRoutes(filter *IpRouteFilter) ([]*DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListDetailedRoutes(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list routes", resp)
|
||||
}
|
||||
|
||||
// AddRoute calls the Tunnelstore POST endpoint for a given route.
|
||||
func (r *RESTClient) AddRoute(newRoute NewRoute) (Route, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(newRoute.Network.String()))
|
||||
resp, err := r.sendRequest("POST", endpoint, newRoute)
|
||||
if err != nil {
|
||||
return Route{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseRoute(resp.Body)
|
||||
}
|
||||
|
||||
return Route{}, r.statusCodeToError("add route", resp)
|
||||
}
|
||||
|
||||
// DeleteRoute calls the Tunnelstore DELETE endpoint for a given route.
|
||||
func (r *RESTClient) DeleteRoute(params DeleteRouteParams) error {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "network", url.PathEscape(params.Network.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
_, err := parseRoute(resp.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
return r.statusCodeToError("delete route", resp)
|
||||
}
|
||||
|
||||
// GetByIP checks which route will proxy a given IP.
|
||||
func (r *RESTClient) GetByIP(params GetRouteByIpParams) (DetailedRoute, error) {
|
||||
endpoint := r.baseEndpoints.accountRoutes
|
||||
endpoint.Path = path.Join(endpoint.Path, "ip", url.PathEscape(params.Ip.String()))
|
||||
setVnetParam(&endpoint, params.VNetID)
|
||||
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return DetailedRoute{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseDetailedRoute(resp.Body)
|
||||
}
|
||||
|
||||
return DetailedRoute{}, r.statusCodeToError("get route by IP", resp)
|
||||
}
|
||||
|
||||
func parseListDetailedRoutes(body io.ReadCloser) ([]*DetailedRoute, error) {
|
||||
var routes []*DetailedRoute
|
||||
err := parseResponse(body, &routes)
|
||||
return routes, err
|
||||
}
|
||||
|
||||
func parseRoute(body io.ReadCloser) (Route, error) {
|
||||
var route Route
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
func parseDetailedRoute(body io.ReadCloser) (DetailedRoute, error) {
|
||||
var route DetailedRoute
|
||||
err := parseResponse(body, &route)
|
||||
return route, err
|
||||
}
|
||||
|
||||
// setVnetParam overwrites the URL's query parameters with a query param to scope the HostnameRoute action to a certain
|
||||
// virtual network (if one is provided).
|
||||
func setVnetParam(endpoint *url.URL, vnetID *uuid.UUID) {
|
||||
queryParams := url.Values{}
|
||||
if vnetID != nil {
|
||||
queryParams.Set("virtual_network_id", vnetID.String())
|
||||
}
|
||||
endpoint.RawQuery = queryParams.Encode()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package teamnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -13,90 +13,90 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
filterDeleted = cli.BoolFlag{
|
||||
filterIpRouteDeleted = cli.BoolFlag{
|
||||
Name: "filter-is-deleted",
|
||||
Usage: "If false (default), only show non-deleted routes. If true, only show deleted routes.",
|
||||
}
|
||||
filterTunnelID = cli.StringFlag{
|
||||
filterIpRouteTunnelID = cli.StringFlag{
|
||||
Name: "filter-tunnel-id",
|
||||
Usage: "Show only routes with the given tunnel ID.",
|
||||
}
|
||||
filterSubset = cli.StringFlag{
|
||||
filterSubsetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-subset-of",
|
||||
Aliases: []string{"nsub"},
|
||||
Usage: "Show only routes whose network is a subset of the given network.",
|
||||
}
|
||||
filterSuperset = cli.StringFlag{
|
||||
filterSupersetIpRoute = cli.StringFlag{
|
||||
Name: "filter-network-is-superset-of",
|
||||
Aliases: []string{"nsup"},
|
||||
Usage: "Show only routes whose network is a superset of the given network.",
|
||||
}
|
||||
filterComment = cli.StringFlag{
|
||||
filterIpRouteComment = cli.StringFlag{
|
||||
Name: "filter-comment-is",
|
||||
Usage: "Show only routes with this comment.",
|
||||
}
|
||||
filterVnet = cli.StringFlag{
|
||||
Name: "filter-virtual-network-id",
|
||||
filterIpRouteByVnet = cli.StringFlag{
|
||||
Name: "filter-vnet-id",
|
||||
Usage: "Show only routes that are attached to the given virtual network ID.",
|
||||
}
|
||||
|
||||
// Flags contains all filter flags.
|
||||
FilterFlags = []cli.Flag{
|
||||
&filterDeleted,
|
||||
&filterTunnelID,
|
||||
&filterSubset,
|
||||
&filterSuperset,
|
||||
&filterComment,
|
||||
&filterVnet,
|
||||
IpRouteFilterFlags = []cli.Flag{
|
||||
&filterIpRouteDeleted,
|
||||
&filterIpRouteTunnelID,
|
||||
&filterSubsetIpRoute,
|
||||
&filterSupersetIpRoute,
|
||||
&filterIpRouteComment,
|
||||
&filterIpRouteByVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// Filter which routes get queried.
|
||||
type Filter struct {
|
||||
// IpRouteFilter which routes get queried.
|
||||
type IpRouteFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||
f := &Filter{
|
||||
// NewIpRouteFilterFromCLI parses CLI flags to discover which filters should get applied.
|
||||
func NewIpRouteFilterFromCLI(c *cli.Context) (*IpRouteFilter, error) {
|
||||
f := &IpRouteFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
|
||||
// Set deletion filter
|
||||
if flag := filterDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
if flag := filterIpRouteDeleted.Name; c.IsSet(flag) && c.Bool(flag) {
|
||||
f.deleted()
|
||||
} else {
|
||||
f.notDeleted()
|
||||
}
|
||||
|
||||
if subset, err := cidrFromFlag(c, filterSubset); err != nil {
|
||||
if subset, err := cidrFromFlag(c, filterSubsetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if subset != nil {
|
||||
f.networkIsSupersetOf(*subset)
|
||||
}
|
||||
|
||||
if superset, err := cidrFromFlag(c, filterSuperset); err != nil {
|
||||
if superset, err := cidrFromFlag(c, filterSupersetIpRoute); err != nil {
|
||||
return nil, err
|
||||
} else if superset != nil {
|
||||
f.networkIsSupersetOf(*superset)
|
||||
}
|
||||
|
||||
if comment := c.String(filterComment.Name); comment != "" {
|
||||
if comment := c.String(filterIpRouteComment.Name); comment != "" {
|
||||
f.commentIs(comment)
|
||||
}
|
||||
|
||||
if tunnelID := c.String(filterTunnelID.Name); tunnelID != "" {
|
||||
if tunnelID := c.String(filterIpRouteTunnelID.Name); tunnelID != "" {
|
||||
u, err := uuid.Parse(tunnelID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterTunnelID.Name)
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteTunnelID.Name)
|
||||
}
|
||||
f.tunnelID(u)
|
||||
}
|
||||
|
||||
if vnetId := c.String(filterVnet.Name); vnetId != "" {
|
||||
if vnetId := c.String(filterIpRouteByVnet.Name); vnetId != "" {
|
||||
u, err := uuid.Parse(vnetId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterVnet.Name)
|
||||
return nil, errors.Wrapf(err, "Couldn't parse UUID from %s", filterIpRouteByVnet.Name)
|
||||
}
|
||||
f.vnetID(u)
|
||||
}
|
||||
@@ -124,42 +124,42 @@ func cidrFromFlag(c *cli.Context, flag cli.StringFlag) (*net.IPNet, error) {
|
||||
return subset, nil
|
||||
}
|
||||
|
||||
func (f *Filter) commentIs(comment string) {
|
||||
func (f *IpRouteFilter) commentIs(comment string) {
|
||||
f.queryParams.Set("comment", comment)
|
||||
}
|
||||
|
||||
func (f *Filter) notDeleted() {
|
||||
func (f *IpRouteFilter) notDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *Filter) deleted() {
|
||||
func (f *IpRouteFilter) deleted() {
|
||||
f.queryParams.Set("is_deleted", "true")
|
||||
}
|
||||
|
||||
func (f *Filter) networkIsSubsetOf(superset net.IPNet) {
|
||||
func (f *IpRouteFilter) networkIsSubsetOf(superset net.IPNet) {
|
||||
f.queryParams.Set("network_subset", superset.String())
|
||||
}
|
||||
|
||||
func (f *Filter) networkIsSupersetOf(subset net.IPNet) {
|
||||
func (f *IpRouteFilter) networkIsSupersetOf(subset net.IPNet) {
|
||||
f.queryParams.Set("network_superset", subset.String())
|
||||
}
|
||||
|
||||
func (f *Filter) existedAt(existedAt time.Time) {
|
||||
func (f *IpRouteFilter) existedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
func (f *Filter) tunnelID(id uuid.UUID) {
|
||||
func (f *IpRouteFilter) tunnelID(id uuid.UUID) {
|
||||
f.queryParams.Set("tunnel_id", id.String())
|
||||
}
|
||||
|
||||
func (f *Filter) vnetID(id uuid.UUID) {
|
||||
func (f *IpRouteFilter) vnetID(id uuid.UUID) {
|
||||
f.queryParams.Set("virtual_network_id", id.String())
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
func (f *IpRouteFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) Encode() string {
|
||||
func (f IpRouteFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package teamnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var ErrTunnelNameConflict = errors.New("tunnel with name already exists")
|
||||
|
||||
type Tunnel struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
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"`
|
||||
IsPendingReconnect bool `json:"is_pending_reconnect"`
|
||||
OriginIP net.IP `json:"origin_ip"`
|
||||
OpenedAt time.Time `json:"opened_at"`
|
||||
}
|
||||
|
||||
type ActiveClient struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Features []string `json:"features"`
|
||||
Version string `json:"version"`
|
||||
Arch string `json:"arch"`
|
||||
RunAt time.Time `json:"run_at"`
|
||||
Connections []Connection `json:"conns"`
|
||||
}
|
||||
|
||||
type newTunnel struct {
|
||||
Name string `json:"name"`
|
||||
TunnelSecret []byte `json:"tunnel_secret"`
|
||||
}
|
||||
|
||||
type CleanupParams struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewCleanupParams() *CleanupParams {
|
||||
return &CleanupParams{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (cp *CleanupParams) ForClient(clientID uuid.UUID) {
|
||||
cp.queryParams.Set("client_id", clientID.String())
|
||||
}
|
||||
|
||||
func (cp CleanupParams) encode() string {
|
||||
return cp.queryParams.Encode()
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateTunnel(name string, tunnelSecret []byte) (*TunnelWithToken, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("tunnel name required")
|
||||
}
|
||||
if _, err := uuid.Parse(name); err == nil {
|
||||
return nil, errors.New("you cannot use UUIDs as tunnel names")
|
||||
}
|
||||
body := &newTunnel{
|
||||
Name: name,
|
||||
TunnelSecret: tunnelSecret,
|
||||
}
|
||||
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountLevel, body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var tunnel TunnelWithToken
|
||||
if serdeErr := parseResponse(resp.Body, &tunnel); err != nil {
|
||||
return nil, serdeErr
|
||||
}
|
||||
return &tunnel, nil
|
||||
case http.StatusConflict:
|
||||
return nil, ErrTunnelNameConflict
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("create tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) GetTunnel(tunnelID uuid.UUID) (*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return unmarshalTunnel(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("get tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("delete tunnel", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListTunnels(filter *TunnelFilter) ([]*Tunnel, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = filter.encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseListTunnels(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list tunnels", resp)
|
||||
}
|
||||
|
||||
func parseListTunnels(body io.ReadCloser) ([]*Tunnel, error) {
|
||||
var tunnels []*Tunnel
|
||||
err := parseResponse(body, &tunnels)
|
||||
return tunnels, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListActiveClients(tunnelID uuid.UUID) ([]*ActiveClient, error) {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
return parseConnectionsDetails(resp.Body)
|
||||
}
|
||||
|
||||
return nil, r.statusCodeToError("list connection details", resp)
|
||||
}
|
||||
|
||||
func parseConnectionsDetails(reader io.Reader) ([]*ActiveClient, error) {
|
||||
var clients []*ActiveClient
|
||||
err := parseResponse(reader, &clients)
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (r *RESTClient) CleanupConnections(tunnelID uuid.UUID, params *CleanupParams) error {
|
||||
endpoint := r.baseEndpoints.accountLevel
|
||||
endpoint.RawQuery = params.encode()
|
||||
endpoint.Path = path.Join(endpoint.Path, fmt.Sprintf("%v/connections", tunnelID))
|
||||
resp, err := r.sendRequest("DELETE", endpoint, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return r.statusCodeToError("cleanup connections", resp)
|
||||
}
|
||||
|
||||
func unmarshalTunnel(reader io.Reader) (*Tunnel, error) {
|
||||
var tunnel Tunnel
|
||||
err := parseResponse(reader, &tunnel)
|
||||
return &tunnel, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package tunnelstore
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
@@ -12,44 +12,44 @@ const (
|
||||
TimeLayout = time.RFC3339
|
||||
)
|
||||
|
||||
type Filter struct {
|
||||
type TunnelFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewFilter() *Filter {
|
||||
return &Filter{
|
||||
func NewTunnelFilter() *TunnelFilter {
|
||||
return &TunnelFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) ByName(name string) {
|
||||
func (f *TunnelFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *Filter) ByNamePrefix(namePrefix string) {
|
||||
func (f *TunnelFilter) ByNamePrefix(namePrefix string) {
|
||||
f.queryParams.Set("name_prefix", namePrefix)
|
||||
}
|
||||
|
||||
func (f *Filter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
func (f *TunnelFilter) ExcludeNameWithPrefix(excludePrefix string) {
|
||||
f.queryParams.Set("exclude_prefix", excludePrefix)
|
||||
}
|
||||
|
||||
func (f *Filter) NoDeleted() {
|
||||
func (f *TunnelFilter) NoDeleted() {
|
||||
f.queryParams.Set("is_deleted", "false")
|
||||
}
|
||||
|
||||
func (f *Filter) ByExistedAt(existedAt time.Time) {
|
||||
func (f *TunnelFilter) ByExistedAt(existedAt time.Time) {
|
||||
f.queryParams.Set("existed_at", existedAt.Format(TimeLayout))
|
||||
}
|
||||
|
||||
func (f *Filter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
func (f *TunnelFilter) ByTunnelID(tunnelID uuid.UUID) {
|
||||
f.queryParams.Set("uuid", tunnelID.String())
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
func (f *TunnelFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) encode() string {
|
||||
func (f TunnelFilter) encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var loc, _ = time.LoadLocation("UTC")
|
||||
|
||||
func Test_parseListTunnels(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []*Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": []}`},
|
||||
want: []*Tunnel{},
|
||||
},
|
||||
{
|
||||
name: "success is false",
|
||||
args: args{body: `{"success": false, "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "errors are present",
|
||||
args: args{body: `{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": []}`},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid response",
|
||||
args: args{body: `abc`},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
body := ioutil.NopCloser(bytes.NewReader([]byte(tt.args.body)))
|
||||
got, err := parseListTunnels(body)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("parseListTunnels() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("parseListTunnels() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_unmarshalTunnel(t *testing.T) {
|
||||
type args struct {
|
||||
body string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Tunnel
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
args: args{body: `{"success": true, "result": {"id":"b34cc7ce-925b-46ee-bc23-4cb5c18d8292","created_at":"2021-07-29T13:46:14.090955Z","deleted_at":"2021-07-29T14:07:27.559047Z","name":"qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV","account_id":6946212,"account_tag":"5ab4e9dfbd435d24068829fda0077963","conns_active_at":null,"conns_inactive_at":"2021-07-29T13:47:22.548482Z","tun_type":"cfd_tunnel","metadata":{"qtid":"a6fJROgkXutNruBGaJjD"}}}`},
|
||||
want: &Tunnel{
|
||||
ID: uuid.MustParse("b34cc7ce-925b-46ee-bc23-4cb5c18d8292"),
|
||||
Name: "qt-bIWWN7D662ogh61pCPfu5s2XgqFY1OyV",
|
||||
CreatedAt: time.Date(2021, 07, 29, 13, 46, 14, 90955000, loc),
|
||||
DeletedAt: time.Date(2021, 07, 29, 14, 7, 27, 559047000, loc),
|
||||
Connections: nil,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := unmarshalTunnel(strings.NewReader(tt.args.body))
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("unmarshalTunnel() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("unmarshalTunnel() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelOk(t *testing.T) {
|
||||
|
||||
jsonBody := `{"success": true, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}`
|
||||
expected := Tunnel{
|
||||
ID: uuid.Nil,
|
||||
Name: "test",
|
||||
CreatedAt: time.Time{},
|
||||
Connections: []Connection{},
|
||||
}
|
||||
actual, err := unmarshalTunnel(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, &expected, actual)
|
||||
}
|
||||
|
||||
func TestUnmarshalTunnelErr(t *testing.T) {
|
||||
|
||||
tests := []string{
|
||||
`abc`,
|
||||
`{"success": true, "result": abc}`,
|
||||
`{"success": false, "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
`{"errors": [{"code": 1003, "message":"An A, AAAA or CNAME record already exists with that host"}], "result": {"id": "00000000-0000-0000-0000-000000000000","name":"test","created_at":"0001-01-01T00:00:00Z","connections":[]}}}`,
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
_, err := unmarshalTunnel(bytes.NewReader([]byte(test)))
|
||||
assert.Error(t, err, fmt.Sprintf("Test #%v failed", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalConnections(t *testing.T) {
|
||||
jsonBody := `{"success":true,"messages":[],"errors":[],"result":[{"id":"d4041254-91e3-4deb-bd94-b46e11680b1e","features":["ha-origin"],"version":"2021.2.5","arch":"darwin_amd64","conns":[{"colo_name":"LIS","id":"ac2286e5-c708-4588-a6a0-ba6b51940019","is_pending_reconnect":false,"origin_ip":"148.38.28.2","opened_at":"0001-01-01T00:00:00Z"}],"run_at":"0001-01-01T00:00:00Z"}]}`
|
||||
expected := ActiveClient{
|
||||
ID: uuid.MustParse("d4041254-91e3-4deb-bd94-b46e11680b1e"),
|
||||
Features: []string{"ha-origin"},
|
||||
Version: "2021.2.5",
|
||||
Arch: "darwin_amd64",
|
||||
RunAt: time.Time{},
|
||||
Connections: []Connection{{
|
||||
ID: uuid.MustParse("ac2286e5-c708-4588-a6a0-ba6b51940019"),
|
||||
ColoName: "LIS",
|
||||
IsPendingReconnect: false,
|
||||
OriginIP: net.ParseIP("148.38.28.2"),
|
||||
OpenedAt: time.Time{},
|
||||
}},
|
||||
}
|
||||
actual, err := parseConnectionsDetails(bytes.NewReader([]byte(jsonBody)))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []*ActiveClient{&expected}, actual)
|
||||
}
|
||||
@@ -1,21 +1,59 @@
|
||||
package tunnelstore
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
)
|
||||
|
||||
func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
||||
type NewVirtualNetwork struct {
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
type VirtualNetwork struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Comment string `json:"comment"`
|
||||
Name string `json:"name"`
|
||||
IsDefault bool `json:"is_default_network"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
DeletedAt time.Time `json:"deleted_at"`
|
||||
}
|
||||
|
||||
type UpdateVirtualNetwork struct {
|
||||
Name *string `json:"name,omitempty"`
|
||||
Comment *string `json:"comment,omitempty"`
|
||||
IsDefault *bool `json:"is_default_network,omitempty"`
|
||||
}
|
||||
|
||||
func (virtualNetwork VirtualNetwork) TableString() string {
|
||||
deletedColumn := "-"
|
||||
if !virtualNetwork.DeletedAt.IsZero() {
|
||||
deletedColumn = virtualNetwork.DeletedAt.Format(time.RFC3339)
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%s\t%s\t%s\t%s\t%s\t%s\t",
|
||||
virtualNetwork.ID,
|
||||
virtualNetwork.Name,
|
||||
strconv.FormatBool(virtualNetwork.IsDefault),
|
||||
virtualNetwork.Comment,
|
||||
virtualNetwork.CreatedAt.Format(time.RFC3339),
|
||||
deletedColumn,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *RESTClient) CreateVirtualNetwork(newVnet NewVirtualNetwork) (VirtualNetwork, error) {
|
||||
resp, err := r.sendRequest("POST", r.baseEndpoints.accountVnets, newVnet)
|
||||
if err != nil {
|
||||
return vnet.VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||
return VirtualNetwork{}, errors.Wrap(err, "REST request failed")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
@@ -23,10 +61,10 @@ func (r *RESTClient) CreateVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.
|
||||
return parseVnet(resp.Body)
|
||||
}
|
||||
|
||||
return vnet.VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||
return VirtualNetwork{}, r.statusCodeToError("add virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) ListVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
||||
func (r *RESTClient) ListVirtualNetworks(filter *VnetFilter) ([]*VirtualNetwork, error) {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.RawQuery = filter.Encode()
|
||||
resp, err := r.sendRequest("GET", endpoint, nil)
|
||||
@@ -59,7 +97,7 @@ func (r *RESTClient) DeleteVirtualNetwork(id uuid.UUID) error {
|
||||
return r.statusCodeToError("delete virtual network", resp)
|
||||
}
|
||||
|
||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
||||
func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates UpdateVirtualNetwork) error {
|
||||
endpoint := r.baseEndpoints.accountVnets
|
||||
endpoint.Path = path.Join(endpoint.Path, url.PathEscape(id.String()))
|
||||
resp, err := r.sendRequest("PATCH", endpoint, updates)
|
||||
@@ -76,14 +114,14 @@ func (r *RESTClient) UpdateVirtualNetwork(id uuid.UUID, updates vnet.UpdateVirtu
|
||||
return r.statusCodeToError("update virtual network", resp)
|
||||
}
|
||||
|
||||
func parseListVnets(body io.ReadCloser) ([]*vnet.VirtualNetwork, error) {
|
||||
var vnets []*vnet.VirtualNetwork
|
||||
func parseListVnets(body io.ReadCloser) ([]*VirtualNetwork, error) {
|
||||
var vnets []*VirtualNetwork
|
||||
err := parseResponse(body, &vnets)
|
||||
return vnets, err
|
||||
}
|
||||
|
||||
func parseVnet(body io.ReadCloser) (vnet.VirtualNetwork, error) {
|
||||
var vnet vnet.VirtualNetwork
|
||||
func parseVnet(body io.ReadCloser) (VirtualNetwork, error) {
|
||||
var vnet VirtualNetwork
|
||||
err := parseResponse(body, &vnet)
|
||||
return vnet, err
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package vnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
@@ -10,68 +10,68 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
filterId = cli.StringFlag{
|
||||
filterVnetId = cli.StringFlag{
|
||||
Name: "id",
|
||||
Usage: "List virtual networks with the given `ID`",
|
||||
}
|
||||
filterName = cli.StringFlag{
|
||||
filterVnetByName = cli.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "List virtual networks with the given `NAME`",
|
||||
}
|
||||
filterDefault = cli.BoolFlag{
|
||||
filterDefaultVnet = cli.BoolFlag{
|
||||
Name: "is-default",
|
||||
Usage: "If true, lists the virtual network that is the default one. If false, lists all non-default virtual networks for the account. If absent, all are included in the results regardless of their default status.",
|
||||
}
|
||||
filterDeleted = cli.BoolFlag{
|
||||
filterDeletedVnet = cli.BoolFlag{
|
||||
Name: "show-deleted",
|
||||
Usage: "If false (default), only show non-deleted virtual networks. If true, only show deleted virtual networks.",
|
||||
}
|
||||
FilterFlags = []cli.Flag{
|
||||
&filterId,
|
||||
&filterName,
|
||||
&filterDefault,
|
||||
&filterDeleted,
|
||||
VnetFilterFlags = []cli.Flag{
|
||||
&filterVnetId,
|
||||
&filterVnetByName,
|
||||
&filterDefaultVnet,
|
||||
&filterDeletedVnet,
|
||||
}
|
||||
)
|
||||
|
||||
// Filter which virtual networks get queried.
|
||||
type Filter struct {
|
||||
// VnetFilter which virtual networks get queried.
|
||||
type VnetFilter struct {
|
||||
queryParams url.Values
|
||||
}
|
||||
|
||||
func NewFilter() *Filter {
|
||||
return &Filter{
|
||||
func NewVnetFilter() *VnetFilter {
|
||||
return &VnetFilter{
|
||||
queryParams: url.Values{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) ById(vnetId uuid.UUID) {
|
||||
func (f *VnetFilter) ById(vnetId uuid.UUID) {
|
||||
f.queryParams.Set("id", vnetId.String())
|
||||
}
|
||||
|
||||
func (f *Filter) ByName(name string) {
|
||||
func (f *VnetFilter) ByName(name string) {
|
||||
f.queryParams.Set("name", name)
|
||||
}
|
||||
|
||||
func (f *Filter) ByDefaultStatus(isDefault bool) {
|
||||
func (f *VnetFilter) ByDefaultStatus(isDefault bool) {
|
||||
f.queryParams.Set("is_default", strconv.FormatBool(isDefault))
|
||||
}
|
||||
|
||||
func (f *Filter) WithDeleted(isDeleted bool) {
|
||||
func (f *VnetFilter) WithDeleted(isDeleted bool) {
|
||||
f.queryParams.Set("is_deleted", strconv.FormatBool(isDeleted))
|
||||
}
|
||||
|
||||
func (f *Filter) MaxFetchSize(max uint) {
|
||||
func (f *VnetFilter) MaxFetchSize(max uint) {
|
||||
f.queryParams.Set("per_page", strconv.Itoa(int(max)))
|
||||
}
|
||||
|
||||
func (f Filter) Encode() string {
|
||||
func (f VnetFilter) Encode() string {
|
||||
return f.queryParams.Encode()
|
||||
}
|
||||
|
||||
// NewFromCLI parses CLI flags to discover which filters should get applied to list virtual networks.
|
||||
func NewFromCLI(c *cli.Context) (*Filter, error) {
|
||||
f := NewFilter()
|
||||
func NewFromCLI(c *cli.Context) (*VnetFilter, error) {
|
||||
f := NewVnetFilter()
|
||||
|
||||
if id := c.String("id"); id != "" {
|
||||
vnetId, err := uuid.Parse(id)
|
||||
@@ -1,4 +1,4 @@
|
||||
package vnet
|
||||
package cfapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
+16
-24
@@ -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
|
||||
@@ -82,9 +83,9 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- make cloudflared-deb
|
||||
build-fips-deb:
|
||||
build-fips-internal-deb:
|
||||
build_dir: *build_dir
|
||||
builddeps:
|
||||
builddeps: &build_fips_deb_deps
|
||||
- *pinned_go_fips
|
||||
- build-essential
|
||||
- fakeroot
|
||||
@@ -93,14 +94,17 @@ stretch: &stretch
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-nightly:
|
||||
build-fips-internal-deb-nightly:
|
||||
build_dir: *build_dir
|
||||
builddeps: *build_deb_deps
|
||||
builddeps: *build_fips_deb_deps
|
||||
post-cache:
|
||||
- export GOOS=linux
|
||||
- export GOARCH=amd64
|
||||
- export NIGHTLY=true
|
||||
- export FIPS=true
|
||||
- export ORIGINAL_NAME=true
|
||||
- make cloudflared-deb
|
||||
build-deb-arm64:
|
||||
build_dir: *build_dir
|
||||
@@ -131,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:
|
||||
@@ -241,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
|
||||
@@ -1,4 +1,4 @@
|
||||
package buildinfo
|
||||
package cliutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -11,23 +11,39 @@ type BuildInfo struct {
|
||||
GoOS string `json:"go_os"`
|
||||
GoVersion string `json:"go_version"`
|
||||
GoArch string `json:"go_arch"`
|
||||
BuildType string `json:"build_type"`
|
||||
CloudflaredVersion string `json:"cloudflared_version"`
|
||||
}
|
||||
|
||||
func GetBuildInfo(cloudflaredVersion string) *BuildInfo {
|
||||
func GetBuildInfo(buildType, version string) *BuildInfo {
|
||||
return &BuildInfo{
|
||||
GoOS: runtime.GOOS,
|
||||
GoVersion: runtime.Version(),
|
||||
GoArch: runtime.GOARCH,
|
||||
CloudflaredVersion: cloudflaredVersion,
|
||||
BuildType: buildType,
|
||||
CloudflaredVersion: version,
|
||||
}
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Log(log *zerolog.Logger) {
|
||||
log.Info().Msgf("Version %s", bi.CloudflaredVersion)
|
||||
if bi.BuildType != "" {
|
||||
log.Info().Msgf("Built%s", bi.GetBuildTypeMsg())
|
||||
}
|
||||
log.Info().Msgf("GOOS: %s, GOVersion: %s, GoArch: %s", bi.GoOS, bi.GoVersion, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) OSArch() string {
|
||||
return fmt.Sprintf("%s_%s", bi.GoOS, bi.GoArch)
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) Version() string {
|
||||
return bi.CloudflaredVersion
|
||||
}
|
||||
|
||||
func (bi *BuildInfo) GetBuildTypeMsg() string {
|
||||
if bi.BuildType == "" {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf(" with %s", bi.BuildType)
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -31,6 +31,7 @@ const (
|
||||
var (
|
||||
Version = "DEV"
|
||||
BuildTime = "unknown"
|
||||
BuildType = ""
|
||||
// Mostly network errors that we don't want reported back to Sentry, this is done by substring match.
|
||||
ignoredErrors = []string{
|
||||
"connection reset by peer",
|
||||
@@ -46,9 +47,10 @@ var (
|
||||
|
||||
func main() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
metrics.RegisterBuildInfo(BuildTime, Version)
|
||||
metrics.RegisterBuildInfo(BuildType, BuildTime, Version)
|
||||
raven.SetRelease(Version)
|
||||
maxprocs.Set()
|
||||
bInfo := cliutil.GetBuildInfo(BuildType, Version)
|
||||
|
||||
// Graceful shutdown channel used by the app. When closed, app must terminate gracefully.
|
||||
// Windows service manager closes this channel when it receives stop command.
|
||||
@@ -67,11 +69,11 @@ 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(),
|
||||
)
|
||||
app.Version = fmt.Sprintf("%s (built %s)", Version, BuildTime)
|
||||
app.Version = fmt.Sprintf("%s (built %s%s)", Version, BuildTime, bInfo.GetBuildTypeMsg())
|
||||
app.Description = `cloudflared connects your machine or user identity to Cloudflare's global network.
|
||||
You can use it to authenticate a session to reach an API behind Access, route web traffic to this machine,
|
||||
and configure access control.
|
||||
@@ -81,7 +83,7 @@ func main() {
|
||||
app.Action = action(graceShutdownC)
|
||||
app.Commands = commands(cli.ShowVersion)
|
||||
|
||||
tunnel.Init(Version, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
tunnel.Init(bInfo, graceShutdownC) // we need this to support the tunnel sub command...
|
||||
access.Init(graceShutdownC)
|
||||
updater.Init(Version)
|
||||
runApp(app, graceShutdownC)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/proxydns"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
|
||||
@@ -31,11 +31,11 @@ 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"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -86,7 +86,7 @@ const (
|
||||
|
||||
var (
|
||||
graceShutdownC chan struct{}
|
||||
version string
|
||||
buildInfo *cliutil.BuildInfo
|
||||
|
||||
routeFailMsg = fmt.Sprintf("failed to provision routing, please create it manually via Cloudflare dashboard or UI; "+
|
||||
"most likely you already have a conflicting record there. You can also rerun this command with --%s to overwrite "+
|
||||
@@ -102,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(),
|
||||
@@ -175,8 +174,8 @@ func TunnelCommand(c *cli.Context) error {
|
||||
return runClassicTunnel(sc)
|
||||
}
|
||||
|
||||
func Init(ver string, gracefulShutdown chan struct{}) {
|
||||
version, graceShutdownC = ver, gracefulShutdown
|
||||
func Init(info *cliutil.BuildInfo, gracefulShutdown chan struct{}) {
|
||||
buildInfo, graceShutdownC = info, gracefulShutdown
|
||||
}
|
||||
|
||||
// runAdhocNamedTunnel create, route and run a named tunnel in one command
|
||||
@@ -209,23 +208,23 @@ func runAdhocNamedTunnel(sc *subcommandContext, name, credentialsOutputPath stri
|
||||
|
||||
// runClassicTunnel creates a "classic" non-named tunnel
|
||||
func runClassicTunnel(sc *subcommandContext) error {
|
||||
return StartServer(sc.c, version, nil, sc.log, sc.isUIEnabled)
|
||||
return StartServer(sc.c, buildInfo, nil, sc.log, sc.isUIEnabled)
|
||||
}
|
||||
|
||||
func routeFromFlag(c *cli.Context) (route tunnelstore.Route, ok bool) {
|
||||
func routeFromFlag(c *cli.Context) (route cfapi.HostnameRoute, ok bool) {
|
||||
if hostname := c.String("hostname"); hostname != "" {
|
||||
if lbPool := c.String("lb-pool"); lbPool != "" {
|
||||
return tunnelstore.NewLBRoute(hostname, lbPool), true
|
||||
return cfapi.NewLBRoute(hostname, lbPool), true
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||
return cfapi.NewDNSRoute(hostname, c.Bool(overwriteDNSFlagName)), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func StartServer(
|
||||
c *cli.Context,
|
||||
version string,
|
||||
namedTunnel *connection.NamedTunnelConfig,
|
||||
info *cliutil.BuildInfo,
|
||||
namedTunnel *connection.NamedTunnelProperties,
|
||||
log *zerolog.Logger,
|
||||
isUIEnabled bool,
|
||||
) error {
|
||||
@@ -271,8 +270,7 @@ func StartServer(
|
||||
defer trace.Stop()
|
||||
}
|
||||
|
||||
buildInfo := buildinfo.GetBuildInfo(version)
|
||||
buildInfo.Log(log)
|
||||
info.Log(log)
|
||||
logClientOptions(c, log)
|
||||
|
||||
// this context drives the server, when it's cancelled tunnel and all other components (origins, dns, etc...) should stop
|
||||
@@ -336,7 +334,7 @@ func StartServer(
|
||||
observer.SendURL(quickTunnelURL)
|
||||
}
|
||||
|
||||
tunnelConfig, ingressRules, err := prepareTunnelConfig(c, buildInfo, version, 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
|
||||
@@ -356,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)
|
||||
@@ -372,15 +371,15 @@ 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 {
|
||||
tunnelUI := ui.NewUIModel(
|
||||
version,
|
||||
info.Version(),
|
||||
hostname,
|
||||
metricsListener.Addr().String(),
|
||||
&ingressRules,
|
||||
dynamicConfig.Ingress,
|
||||
tunnelConfig.HAConnections,
|
||||
)
|
||||
app := tunnelUI.Launch(ctx, log, logTransport)
|
||||
@@ -725,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,
|
||||
}),
|
||||
@@ -779,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,
|
||||
}),
|
||||
@@ -797,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,
|
||||
}),
|
||||
@@ -817,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{
|
||||
@@ -1001,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() {
|
||||
@@ -1012,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 {
|
||||
|
||||
@@ -16,13 +16,15 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/buildinfo"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"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"
|
||||
@@ -86,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)
|
||||
}
|
||||
|
||||
@@ -148,60 +150,63 @@ func getOriginCert(originCertPath string, log *zerolog.Logger) ([]byte, error) {
|
||||
|
||||
func prepareTunnelConfig(
|
||||
c *cli.Context,
|
||||
buildInfo *buildinfo.BuildInfo,
|
||||
version string,
|
||||
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),
|
||||
Version: version,
|
||||
Arch: buildInfo.OSArch(),
|
||||
Version: info.Version(),
|
||||
Arch: info.OSArch(),
|
||||
}
|
||||
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: buildInfo.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: 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) {
|
||||
|
||||
@@ -5,12 +5,12 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
type Info struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Connectors []*tunnelstore.ActiveClient `json:"conns"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Connectors []*cfapi.ActiveClient `json:"conns"`
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -76,8 +75,8 @@ func RunQuickTunnel(sc *subcommandContext) error {
|
||||
|
||||
return StartServer(
|
||||
sc.c,
|
||||
version,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
buildInfo,
|
||||
&connection.NamedTunnelProperties{Credentials: credentials, QuickTunnelUrl: data.Result.Hostname},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
|
||||
@@ -14,9 +14,9 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/certutil"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/logger"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
type errInvalidJSONCredential struct {
|
||||
@@ -37,7 +37,7 @@ type subcommandContext struct {
|
||||
fs fileSystem
|
||||
|
||||
// These fields should be accessed using their respective Getter
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ type userCredential struct {
|
||||
certPath string
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||
func (sc *subcommandContext) client() (cfapi.Client, error) {
|
||||
if sc.tunnelstoreClient != nil {
|
||||
return sc.tunnelstoreClient, nil
|
||||
}
|
||||
@@ -76,8 +76,8 @@ func (sc *subcommandContext) client() (tunnelstore.Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", version)
|
||||
client, err := tunnelstore.NewRESTClient(
|
||||
userAgent := fmt.Sprintf("cloudflared/%s", buildInfo.Version())
|
||||
client, err := cfapi.NewRESTClient(
|
||||
sc.c.String("api-url"),
|
||||
credential.cert.AccountID,
|
||||
credential.cert.ZoneID,
|
||||
@@ -149,7 +149,7 @@ func (sc *subcommandContext) readTunnelCredentials(credFinder CredFinder) (conne
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*tunnelstore.Tunnel, error) {
|
||||
func (sc *subcommandContext) create(name string, credentialsFilePath string, secret string) (*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "couldn't create client to talk to Cloudflare Tunnel backend")
|
||||
@@ -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,10 +220,11 @@ 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 *tunnelstore.Filter) ([]*tunnelstore.Tunnel, error) {
|
||||
func (sc *subcommandContext) list(filter *cfapi.TunnelFilter) ([]*cfapi.Tunnel, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -251,7 +251,7 @@ func (sc *subcommandContext) delete(tunnelIDs []uuid.UUID) error {
|
||||
return fmt.Errorf("Tunnel %s has already been deleted", tunnel.ID)
|
||||
}
|
||||
if forceFlagSet {
|
||||
if err := client.CleanupConnections(tunnel.ID, tunnelstore.NewCleanupParams()); err != nil {
|
||||
if err := client.CleanupConnections(tunnel.ID, cfapi.NewCleanupParams()); err != nil {
|
||||
return errors.Wrapf(err, "Error cleaning up connections for tunnel %s", tunnel.ID)
|
||||
}
|
||||
}
|
||||
@@ -301,17 +301,23 @@ 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,
|
||||
version,
|
||||
&connection.NamedTunnelConfig{Credentials: credentials},
|
||||
buildInfo,
|
||||
&connection.NamedTunnelProperties{Credentials: credentials},
|
||||
sc.log,
|
||||
sc.isUIEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
params := tunnelstore.NewCleanupParams()
|
||||
params := cfapi.NewCleanupParams()
|
||||
extraLog := ""
|
||||
if connector := sc.c.String("connector-id"); connector != "" {
|
||||
connectorID, err := uuid.Parse(connector)
|
||||
@@ -335,7 +341,7 @@ func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tunnelstore.RouteResult, error) {
|
||||
func (sc *subcommandContext) route(tunnelID uuid.UUID, r cfapi.HostnameRoute) (cfapi.HostnameRouteResult, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -345,8 +351,8 @@ func (sc *subcommandContext) route(tunnelID uuid.UUID, r tunnelstore.Route) (tun
|
||||
}
|
||||
|
||||
// Query Tunnelstore to find the active tunnel with the given name.
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*tunnelstore.Tunnel, bool, error) {
|
||||
filter := tunnelstore.NewFilter()
|
||||
func (sc *subcommandContext) tunnelActive(name string) (*cfapi.Tunnel, bool, error) {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
tunnels, err := sc.list(filter)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -391,7 +397,7 @@ func (sc *subcommandContext) findIDs(inputs []string) ([]uuid.UUID, error) {
|
||||
uuids, names := splitUuids(inputs)
|
||||
|
||||
for _, name := range names {
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.NoDeleted()
|
||||
filter.ByName(name)
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@ package tunnel
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
const noClientMsg = "error while creating backend client"
|
||||
|
||||
func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.DetailedRoute, error) {
|
||||
func (sc *subcommandContext) listRoutes(filter *cfapi.IpRouteFilter) ([]*cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
@@ -16,15 +16,15 @@ func (sc *subcommandContext) listRoutes(filter *teamnet.Filter) ([]*teamnet.Deta
|
||||
return client.ListRoutes(filter)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) addRoute(newRoute teamnet.NewRoute) (teamnet.Route, error) {
|
||||
func (sc *subcommandContext) addRoute(newRoute cfapi.NewRoute) (cfapi.Route, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return teamnet.Route{}, errors.Wrap(err, noClientMsg)
|
||||
return cfapi.Route{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.AddRoute(newRoute)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error {
|
||||
func (sc *subcommandContext) deleteRoute(params cfapi.DeleteRouteParams) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
@@ -32,10 +32,10 @@ func (sc *subcommandContext) deleteRoute(params teamnet.DeleteRouteParams) error
|
||||
return client.DeleteRoute(params)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) getRouteByIP(params teamnet.GetRouteByIpParams) (teamnet.DetailedRoute, error) {
|
||||
func (sc *subcommandContext) getRouteByIP(params cfapi.GetRouteByIpParams) (cfapi.DetailedRoute, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return teamnet.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
return cfapi.DetailedRoute{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.GetByIP(params)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ 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"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
type mockFileSystem struct {
|
||||
@@ -36,7 +37,7 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
log *zerolog.Logger
|
||||
isUIEnabled bool
|
||||
fs fileSystem
|
||||
tunnelstoreClient tunnelstore.Client
|
||||
tunnelstoreClient cfapi.Client
|
||||
userCredential *userCredential
|
||||
}
|
||||
type args struct {
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -187,13 +186,13 @@ func Test_subcommandContext_findCredentials(t *testing.T) {
|
||||
}
|
||||
|
||||
type deleteMockTunnelStore struct {
|
||||
tunnelstore.Client
|
||||
cfapi.Client
|
||||
mockTunnels map[uuid.UUID]mockTunnelBehaviour
|
||||
deletedTunnelIDs []uuid.UUID
|
||||
}
|
||||
|
||||
type mockTunnelBehaviour struct {
|
||||
tunnel tunnelstore.Tunnel
|
||||
tunnel cfapi.Tunnel
|
||||
deleteErr error
|
||||
cleanupErr error
|
||||
}
|
||||
@@ -209,7 +208,7 @@ func newDeleteMockTunnelStore(tunnels ...mockTunnelBehaviour) *deleteMockTunnelS
|
||||
}
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
||||
func (d *deleteMockTunnelStore) GetTunnel(tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
@@ -233,7 +232,7 @@ func (d *deleteMockTunnelStore) DeleteTunnel(tunnelID uuid.UUID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *tunnelstore.CleanupParams) error {
|
||||
func (d *deleteMockTunnelStore) CleanupConnections(tunnelID uuid.UUID, _ *cfapi.CleanupParams) error {
|
||||
tunnel, ok := d.mockTunnels[tunnelID]
|
||||
if !ok {
|
||||
return fmt.Errorf("Couldn't find tunnel: %v", tunnelID)
|
||||
@@ -284,10 +283,10 @@ func Test_subcommandContext_Delete(t *testing.T) {
|
||||
}(),
|
||||
tunnelstoreClient: newDeleteMockTunnelStore(
|
||||
mockTunnelBehaviour{
|
||||
tunnel: tunnelstore.Tunnel{ID: tunnelID1},
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID1},
|
||||
},
|
||||
mockTunnelBehaviour{
|
||||
tunnel: tunnelstore.Tunnel{ID: tunnelID2},
|
||||
tunnel: cfapi.Tunnel{ID: tunnelID2},
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,18 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
)
|
||||
|
||||
func (sc *subcommandContext) addVirtualNetwork(newVnet vnet.NewVirtualNetwork) (vnet.VirtualNetwork, error) {
|
||||
func (sc *subcommandContext) addVirtualNetwork(newVnet cfapi.NewVirtualNetwork) (cfapi.VirtualNetwork, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return vnet.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||
return cfapi.VirtualNetwork{}, errors.Wrap(err, noClientMsg)
|
||||
}
|
||||
return client.CreateVirtualNetwork(newVnet)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) listVirtualNetworks(filter *vnet.Filter) ([]*vnet.VirtualNetwork, error) {
|
||||
func (sc *subcommandContext) listVirtualNetworks(filter *cfapi.VnetFilter) ([]*cfapi.VirtualNetwork, error) {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, noClientMsg)
|
||||
@@ -31,7 +31,7 @@ func (sc *subcommandContext) deleteVirtualNetwork(vnetId uuid.UUID) error {
|
||||
return client.DeleteVirtualNetwork(vnetId)
|
||||
}
|
||||
|
||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates vnet.UpdateVirtualNetwork) error {
|
||||
func (sc *subcommandContext) updateVirtualNetwork(vnetId uuid.UUID, updates cfapi.UpdateVirtualNetwork) error {
|
||||
client, err := sc.client()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, noClientMsg)
|
||||
|
||||
@@ -2,6 +2,7 @@ package tunnel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -21,11 +22,11 @@ import (
|
||||
"golang.org/x/net/idna"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/config"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
"github.com/cloudflare/cloudflared/tunnelstore"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -34,6 +35,7 @@ const (
|
||||
CredFileFlagAlias = "cred-file"
|
||||
CredFileFlag = "credentials-file"
|
||||
CredContentsFlag = "credentials-contents"
|
||||
TunnelTokenFlag = "token"
|
||||
overwriteDNSFlagName = "overwrite-dns"
|
||||
|
||||
LogFieldTunnelID = "tunnelID"
|
||||
@@ -64,8 +66,8 @@ var (
|
||||
Name: "when",
|
||||
Aliases: []string{"w"},
|
||||
Usage: "List tunnels that are active at the given `TIME` in RFC3339 format",
|
||||
Layout: tunnelstore.TimeLayout,
|
||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(tunnelstore.TimeLayout)),
|
||||
Layout: cfapi.TimeLayout,
|
||||
DefaultText: fmt.Sprintf("current time, %s", time.Now().Format(cfapi.TimeLayout)),
|
||||
}
|
||||
listIDFlag = &cli.StringFlag{
|
||||
Name: "id",
|
||||
@@ -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"},
|
||||
@@ -260,7 +267,7 @@ func listCommand(c *cli.Context) error {
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
filter := tunnelstore.NewFilter()
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
if !c.Bool("show-deleted") {
|
||||
filter.NoDeleted()
|
||||
}
|
||||
@@ -335,7 +342,7 @@ func listCommand(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconnected bool) {
|
||||
func formatAndPrintTunnelList(tunnels []*cfapi.Tunnel, showRecentlyDisconnected bool) {
|
||||
writer := tabWriter()
|
||||
defer writer.Flush()
|
||||
|
||||
@@ -357,7 +364,7 @@ func formatAndPrintTunnelList(tunnels []*tunnelstore.Tunnel, showRecentlyDisconn
|
||||
}
|
||||
}
|
||||
|
||||
func fmtConnections(connections []tunnelstore.Connection, showRecentlyDisconnected bool) string {
|
||||
func fmtConnections(connections []cfapi.Connection, showRecentlyDisconnected bool) string {
|
||||
|
||||
// Count connections per colo
|
||||
numConnsPerColo := make(map[string]uint, len(connections))
|
||||
@@ -477,8 +484,8 @@ func tunnelInfo(c *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*tunnelstore.Tunnel, error) {
|
||||
filter := tunnelstore.NewFilter()
|
||||
func getTunnel(sc *subcommandContext, tunnelID uuid.UUID) (*cfapi.Tunnel, error) {
|
||||
filter := cfapi.NewTunnelFilter()
|
||||
filter.ByTunnelID(tunnelID)
|
||||
tunnels, err := sc.list(filter)
|
||||
if err != nil {
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -711,7 +739,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
{
|
||||
Name: "dns",
|
||||
Action: cliutil.ConfiguredAction(routeDnsCommand),
|
||||
Usage: "Route a hostname by creating a DNS CNAME record to a tunnel",
|
||||
Usage: "HostnameRoute a hostname by creating a DNS CNAME record to a tunnel",
|
||||
UsageText: "cloudflared tunnel route dns [TUNNEL] [HOSTNAME]",
|
||||
Description: `Creates a DNS CNAME record hostname that points to the tunnel.`,
|
||||
Flags: []cli.Flag{overwriteDNSFlag},
|
||||
@@ -728,7 +756,7 @@ Further information about managing Cloudflare WARP traffic to your tunnel is ava
|
||||
}
|
||||
}
|
||||
|
||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route, error) {
|
||||
func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (cfapi.HostnameRoute, error) {
|
||||
const (
|
||||
userHostnameIndex = 1
|
||||
expectedNArgs = 2
|
||||
@@ -742,10 +770,10 @@ func dnsRouteFromArg(c *cli.Context, overwriteExisting bool) (tunnelstore.Route,
|
||||
} else if !validateHostname(userHostname, true) {
|
||||
return nil, errors.Errorf("%s is not a valid hostname", userHostname)
|
||||
}
|
||||
return tunnelstore.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||
return cfapi.NewDNSRoute(userHostname, overwriteExisting), nil
|
||||
}
|
||||
|
||||
func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
func lbRouteFromArg(c *cli.Context) (cfapi.HostnameRoute, error) {
|
||||
const (
|
||||
lbNameIndex = 1
|
||||
lbPoolIndex = 2
|
||||
@@ -768,7 +796,7 @@ func lbRouteFromArg(c *cli.Context) (tunnelstore.Route, error) {
|
||||
return nil, errors.Errorf("%s is not a valid pool name", lbPool)
|
||||
}
|
||||
|
||||
return tunnelstore.NewLBRoute(lbName, lbPool), nil
|
||||
return cfapi.NewLBRoute(lbName, lbPool), nil
|
||||
}
|
||||
|
||||
var nameRegex = regexp.MustCompile("^[_a-zA-Z0-9][-_.a-zA-Z0-9]*$")
|
||||
@@ -815,7 +843,7 @@ func routeCommand(c *cli.Context, routeType string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var route tunnelstore.Route
|
||||
var route cfapi.HostnameRoute
|
||||
switch routeType {
|
||||
case "dns":
|
||||
route, err = dnsRouteFromArg(c, c.Bool(overwriteDNSFlagName))
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
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/tunnelstore"
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/connection"
|
||||
)
|
||||
|
||||
func Test_fmtConnections(t *testing.T) {
|
||||
type args struct {
|
||||
connections []tunnelstore.Connection
|
||||
connections []cfapi.Connection
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -23,14 +27,14 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{},
|
||||
connections: []cfapi.Connection{},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "trivial",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -42,7 +46,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "with a pending reconnect",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "DFW",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -55,7 +59,7 @@ func Test_fmtConnections(t *testing.T) {
|
||||
{
|
||||
name: "many colos",
|
||||
args: args{
|
||||
connections: []tunnelstore.Connection{
|
||||
connections: []cfapi.Connection{
|
||||
{
|
||||
ColoName: "YRV",
|
||||
ID: uuid.MustParse("ea550130-57fd-4463-aab1-752822231ddd"),
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -8,17 +8,16 @@ import (
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/teamnet"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
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.",
|
||||
}
|
||||
@@ -36,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",
|
||||
@@ -51,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},
|
||||
},
|
||||
{
|
||||
@@ -92,7 +91,7 @@ to tell which virtual network whose routing table you want to use.`,
|
||||
|
||||
func showRoutesFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, teamnet.FilterFlags...)
|
||||
flags = append(flags, cfapi.IpRouteFilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
@@ -103,7 +102,7 @@ func showRoutesCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
filter, err := teamnet.NewFromCLI(c)
|
||||
filter, err := cfapi.NewIpRouteFilterFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid config for routing filters")
|
||||
}
|
||||
@@ -168,7 +167,7 @@ func addRouteCommand(c *cli.Context) error {
|
||||
vnetId = &id
|
||||
}
|
||||
|
||||
_, err = sc.addRoute(teamnet.NewRoute{
|
||||
_, err = sc.addRoute(cfapi.NewRoute{
|
||||
Comment: comment,
|
||||
Network: *network,
|
||||
TunnelID: tunnelID,
|
||||
@@ -199,7 +198,7 @@ func deleteRouteCommand(c *cli.Context) error {
|
||||
return errors.New("Invalid network CIDR")
|
||||
}
|
||||
|
||||
params := teamnet.DeleteRouteParams{
|
||||
params := cfapi.DeleteRouteParams{
|
||||
Network: *network,
|
||||
}
|
||||
|
||||
@@ -233,7 +232,7 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||
return fmt.Errorf("Invalid IP %s", ipInput)
|
||||
}
|
||||
|
||||
params := teamnet.GetRouteByIpParams{
|
||||
params := cfapi.GetRouteByIpParams{
|
||||
Ip: ip,
|
||||
}
|
||||
|
||||
@@ -252,12 +251,12 @@ func getRouteByIPCommand(c *cli.Context) error {
|
||||
if route.IsZero() {
|
||||
fmt.Printf("No route matches the IP %s\n", ip)
|
||||
} else {
|
||||
formatAndPrintRouteList([]*teamnet.DetailedRoute{&route})
|
||||
formatAndPrintRouteList([]*cfapi.DetailedRoute{&route})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatAndPrintRouteList(routes []*teamnet.DetailedRoute) {
|
||||
func formatAndPrintRouteList(routes []*cfapi.DetailedRoute) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/cloudflare/cloudflared/cfapi"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/cliutil"
|
||||
"github.com/cloudflare/cloudflared/cmd/cloudflared/updater"
|
||||
"github.com/cloudflare/cloudflared/vnet"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -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
|
||||
@@ -102,7 +102,7 @@ default or update an existing one to become the default.`,
|
||||
|
||||
func listVirtualNetworksFlags() []cli.Flag {
|
||||
flags := make([]cli.Flag, 0)
|
||||
flags = append(flags, vnet.FilterFlags...)
|
||||
flags = append(flags, cfapi.VnetFilterFlags...)
|
||||
flags = append(flags, outputFormatFlag)
|
||||
return flags
|
||||
}
|
||||
@@ -128,7 +128,7 @@ func addVirtualNetworkCommand(c *cli.Context) error {
|
||||
comment = args.Get(1)
|
||||
}
|
||||
|
||||
newVnet := vnet.NewVirtualNetwork{
|
||||
newVnet := cfapi.NewVirtualNetwork{
|
||||
Name: name,
|
||||
Comment: comment,
|
||||
IsDefault: c.Bool(makeDefaultFlag.Name),
|
||||
@@ -160,7 +160,7 @@ func listVirtualNetworksCommand(c *cli.Context) error {
|
||||
warningChecker := updater.StartWarningCheck(c)
|
||||
defer warningChecker.LogWarningIfAny(sc.log)
|
||||
|
||||
filter, err := vnet.NewFromCLI(c)
|
||||
filter, err := cfapi.NewFromCLI(c)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid flags for filtering virtual networks")
|
||||
}
|
||||
@@ -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
|
||||
@@ -220,7 +220,7 @@ func updateVirtualNetworkCommand(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
updates := vnet.UpdateVirtualNetwork{}
|
||||
updates := cfapi.UpdateVirtualNetwork{}
|
||||
|
||||
if c.IsSet(newNameFlag.Name) {
|
||||
newName := c.String(newNameFlag.Name)
|
||||
@@ -248,7 +248,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
filter := vnet.NewFilter()
|
||||
filter := cfapi.NewVnetFilter()
|
||||
filter.WithDeleted(false)
|
||||
filter.ByName(input)
|
||||
|
||||
@@ -264,7 +264,7 @@ func getVnetId(sc *subcommandContext, input string) (uuid.UUID, error) {
|
||||
return vnets[0].ID, nil
|
||||
}
|
||||
|
||||
func formatAndPrintVnetsList(vnets []*vnet.VirtualNetwork) {
|
||||
func formatAndPrintVnetsList(vnets []*cfapi.VirtualNetwork) {
|
||||
const (
|
||||
minWidth = 0
|
||||
tabWidth = 8
|
||||
|
||||
@@ -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())
|
||||
|
||||
|
||||
+69
-35
@@ -29,84 +29,111 @@ const (
|
||||
// HTTPMethodKey is used to get or set http method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPMethodKey = "HttpMethod"
|
||||
// HTTPHostKey is used to get or set http Method in QUIC ALPN if the underlying proxy connection type is HTTP.
|
||||
HTTPHostKey = "HttpHost"
|
||||
MaxDatagramFrameSize = 1220
|
||||
HTTPHostKey = "HttpHost"
|
||||
)
|
||||
|
||||
// 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")
|
||||
}
|
||||
@@ -119,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
|
||||
@@ -148,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)
|
||||
@@ -156,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
|
||||
@@ -199,10 +230,8 @@ func (q *QUICConnection) serveUDPSession(session *datagramsession.Session, close
|
||||
} else {
|
||||
q.closeUDPSession(ctx, session.ID, "terminated without error")
|
||||
}
|
||||
q.logger.Debug().Err(err).Str("sessionID", session.ID.String()).Msg("session terminated")
|
||||
return
|
||||
}
|
||||
q.logger.Debug().Err(err).Msg("Session terminated by edge")
|
||||
q.logger.Debug().Err(err).Str("sessionID", session.ID.String()).Msg("Session terminated")
|
||||
}
|
||||
|
||||
// closeUDPSession first unregisters the session from session manager, then it tries to unregister from edge
|
||||
@@ -235,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 {
|
||||
@@ -308,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:
|
||||
@@ -95,7 +100,7 @@ func (m *manager) RegisterSession(ctx context.Context, sessionID uuid.UUID, orig
|
||||
}
|
||||
|
||||
func (m *manager) registerSession(ctx context.Context, registration *registerSessionEvent) {
|
||||
session := newSession(registration.sessionID, m.transport, registration.originProxy)
|
||||
session := newSession(registration.sessionID, m.transport, registration.originProxy, m.log)
|
||||
m.sessions[registration.sessionID] = session
|
||||
registration.resultChan <- session
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -17,7 +18,7 @@ func SessionIdleErr(timeout time.Duration) error {
|
||||
return fmt.Errorf("session idle for %v", timeout)
|
||||
}
|
||||
|
||||
// Each Session is a bidirectional pipe of datagrams between transport and dstConn
|
||||
// Session is a bidirectional pipe of datagrams between transport and dstConn
|
||||
// Currently the only implementation of transport is quic DatagramMuxer
|
||||
// Destination can be a connection with origin or with eyeball
|
||||
// When the destination is origin:
|
||||
@@ -35,9 +36,10 @@ type Session struct {
|
||||
// activeAtChan is used to communicate the last read/write time
|
||||
activeAtChan chan time.Time
|
||||
closeChan chan error
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *Session {
|
||||
func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser, log *zerolog.Logger) *Session {
|
||||
return &Session{
|
||||
ID: id,
|
||||
transport: transport,
|
||||
@@ -47,6 +49,7 @@ func newSession(id uuid.UUID, transport transport, dstConn io.ReadWriteCloser) *
|
||||
activeAtChan: make(chan time.Time, 2),
|
||||
// capacity is 2 because close() and dstToTransport routine in Serve() can write to this channel
|
||||
closeChan: make(chan error, 2),
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +57,8 @@ func (s *Session) Serve(ctx context.Context, closeAfterIdle time.Duration) (clos
|
||||
go func() {
|
||||
// QUIC implementation copies data to another buffer before returning https://github.com/lucas-clemente/quic-go/blob/v0.24.0/session.go#L1967-L1975
|
||||
// This makes it safe to share readBuffer between iterations
|
||||
readBuffer := make([]byte, s.transport.MTU())
|
||||
const maxPacketSize = 1500
|
||||
readBuffer := make([]byte, maxPacketSize)
|
||||
for {
|
||||
if err := s.dstToTransport(readBuffer); err != nil {
|
||||
s.closeChan <- err
|
||||
@@ -102,11 +106,23 @@ 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 err := s.transport.SendTo(s.ID, buffer[:n]); err != nil {
|
||||
return err
|
||||
if n <= int(s.transport.MTU()) {
|
||||
err = s.transport.SendTo(s.ID, buffer[:n])
|
||||
} else {
|
||||
// drop packet for now, eventually reply with ICMP for PMTUD
|
||||
s.log.Debug().
|
||||
Str("session", s.ID.String()).
|
||||
Int("len", n).
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
@@ -44,7 +45,8 @@ func testSessionReturns(t *testing.T, closeBy closeMethod, closeAfterIdle time.D
|
||||
reqChan: newDatagramChannel(1),
|
||||
respChan: newDatagramChannel(1),
|
||||
}
|
||||
session := newSession(sessionID, transport, cfdConn)
|
||||
log := zerolog.Nop()
|
||||
session := newSession(sessionID, transport, cfdConn, &log)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
sessionDone := make(chan struct{})
|
||||
@@ -119,7 +121,8 @@ func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool)
|
||||
reqChan: newDatagramChannel(100),
|
||||
respChan: newDatagramChannel(100),
|
||||
}
|
||||
session := newSession(sessionID, transport, cfdConn)
|
||||
log := zerolog.Nop()
|
||||
session := newSession(sessionID, transport, cfdConn, &log)
|
||||
|
||||
startTime := time.Now()
|
||||
activeUntil := startTime.Add(activeTime)
|
||||
@@ -181,7 +184,7 @@ func testActiveSessionNotClosed(t *testing.T, readFromDst bool, writeToDst bool)
|
||||
|
||||
func TestMarkActiveNotBlocking(t *testing.T) {
|
||||
const concurrentCalls = 50
|
||||
session := newSession(uuid.New(), nil, nil)
|
||||
session := newSession(uuid.New(), nil, nil, nil)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(concurrentCalls)
|
||||
for i := 0; i < concurrentCalls; i++ {
|
||||
@@ -192,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())
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@ type transport interface {
|
||||
SendTo(sessionID uuid.UUID, payload []byte) error
|
||||
// ReceiveFrom reads the next datagram from the transport
|
||||
ReceiveFrom() (uuid.UUID, []byte, error)
|
||||
// Max transmission unit of the transport
|
||||
MTU() uint
|
||||
// Max transmission unit to receive from the transport
|
||||
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 1220
|
||||
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
|
||||
}
|
||||
|
||||
+3
-3
@@ -83,15 +83,15 @@ func ServeMetrics(
|
||||
return err
|
||||
}
|
||||
|
||||
func RegisterBuildInfo(buildTime string, version string) {
|
||||
func RegisterBuildInfo(buildType, buildTime, version string) {
|
||||
buildInfo := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
// Don't namespace build_info, since we want it to be consistent across all Cloudflare services
|
||||
Name: "build_info",
|
||||
Help: "Build and version information",
|
||||
},
|
||||
[]string{"goversion", "revision", "version"},
|
||||
[]string{"goversion", "type", "revision", "version"},
|
||||
)
|
||||
prometheus.MustRegister(buildInfo)
|
||||
buildInfo.WithLabelValues(runtime.Version(), buildTime, version).Set(1)
|
||||
buildInfo.WithLabelValues(runtime.Version(), buildType, buildTime, version).Set(1)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+11
-11
@@ -9,8 +9,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
MaxDatagramFrameSize = 1220
|
||||
sessionIDLen = len(uuid.UUID{})
|
||||
sessionIDLen = len(uuid.UUID{})
|
||||
)
|
||||
|
||||
type DatagramMuxer struct {
|
||||
@@ -32,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), MaxDatagramFrameSize-sessionIDLen)
|
||||
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")
|
||||
}
|
||||
@@ -54,16 +53,17 @@ func (dm *DatagramMuxer) ReceiveFrom() (uuid.UUID, []byte, error) {
|
||||
if err != nil {
|
||||
return uuid.Nil, nil, err
|
||||
}
|
||||
return ExtractSessionID(msg)
|
||||
return extractSessionID(msg)
|
||||
}
|
||||
|
||||
func (dm *DatagramMuxer) MTU() uint {
|
||||
return MaxDatagramFrameSize
|
||||
// Maximum application payload to send to / receive from QUIC datagram frame
|
||||
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))
|
||||
@@ -79,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)
|
||||
}
|
||||
|
||||
+117
-5
@@ -1,10 +1,21 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/lucas-clemente/quic-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -13,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)
|
||||
@@ -26,16 +37,117 @@ 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, maxDatagramPayloadSize)
|
||||
|
||||
quicConfig := &quic.Config{
|
||||
KeepAlive: true,
|
||||
EnableDatagrams: true,
|
||||
MaxDatagramFrameSize: MaxDatagramFrameSize,
|
||||
}
|
||||
quicListener := newQUICListener(t, quicConfig)
|
||||
defer quicListener.Close()
|
||||
|
||||
errGroup, ctx := errgroup.WithContext(context.Background())
|
||||
// Run edge side of datagram muxer
|
||||
errGroup.Go(func() error {
|
||||
// Accept quic connection
|
||||
quicSession, err := quicListener.Accept(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
muxer, err := NewDatagramMuxer(quicSession)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sessionID, receivedPayload, err := muxer.ReceiveFrom()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
require.Equal(t, testSessionID, sessionID)
|
||||
require.True(t, bytes.Equal(payload, receivedPayload))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// Run cloudflared side of datagram muxer
|
||||
errGroup.Go(func() error {
|
||||
tlsClientConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"argotunnel"},
|
||||
}
|
||||
// Establish quic connection
|
||||
quicSession, err := quic.DialAddrEarly(quicListener.Addr().String(), tlsClientConfig, quicConfig)
|
||||
require.NoError(t, err)
|
||||
|
||||
muxer, err := NewDatagramMuxer(quicSession)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Payload larger than transport MTU, should return an error
|
||||
largePayload := make([]byte, MaxDatagramFrameSize)
|
||||
err = muxer.SendTo(testSessionID, largePayload)
|
||||
require.Error(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
require.NoError(t, errGroup.Wait())
|
||||
}
|
||||
|
||||
func newQUICListener(t *testing.T, config *quic.Config) quic.Listener {
|
||||
// Create a simple tls config.
|
||||
tlsConfig := generateTLSConfig()
|
||||
|
||||
listener, err := quic.ListenAddr("127.0.0.1:0", tlsConfig, config)
|
||||
require.NoError(t, err)
|
||||
|
||||
return listener
|
||||
}
|
||||
|
||||
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"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user