mirror of
https://github.com/openziti/ziti.git
synced 2026-10-12 23:32:02 +00:00
Initial migration from internal repositories.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"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" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"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.
|
||||
|
||||
"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).
|
||||
|
||||
"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.
|
||||
|
||||
"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."
|
||||
|
||||
"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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(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
|
||||
|
||||
(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.
|
||||
|
||||
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. 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.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Ziti Overview
|
||||
|
||||
Ziti is split into two main domains that run in the same process space:
|
||||
|
||||
- Ziti Fabric
|
||||
- Ziti Edge
|
||||
|
||||
## Fabric & Edge
|
||||
The Ziti Fabric is a core set of features used to support defining and managing services, routers, and
|
||||
sessions to route traffic across a robust and secure overlay network. Ziti Fabric features are always enabled and
|
||||
cannot be disabled.
|
||||
|
||||
The Ziti Edge is a set of features that can be enabled on top of the Ziti Fabric features to enable enrollment
|
||||
and management of endpoints that make use of the Ziti SDK. The Ziti SDK can be built into applications to provide
|
||||
ingress and egress to the Ziti overlay network as well as to provide application specific networking to an individual
|
||||
application. Enabling the Edge features is optional.
|
||||
|
||||
Both the Fabric and Edge features are built into the ziti-controller and ziti-router binaries.
|
||||
|
||||
## Ziti Controller
|
||||
The Ziti Controller (ziti-controller) is the main server component of a Ziti environment. It is the first piece of Ziti
|
||||
that must be setup and configured. The controller houses all the router, service, and management data necessary
|
||||
to run a Ziti environment. There is one, and only one, controller per Ziti environment.
|
||||
|
||||
The Ziti Controller can optionally host the Ziti Edge features. The Fabric features within the controller
|
||||
supports managing routers, services, and creating circuits across a mesh network to route traffic, but does not support
|
||||
accepting connections from endpoints utilizing the Ziti SDK, provide a configurable policy management for endpoint
|
||||
connectivity, and endpoint enrollment.
|
||||
|
||||
## Ziti Router
|
||||
The Ziti Router binary (ziti-router) is deployed multiple times to stand up multiple ingress and egress
|
||||
points for a Ziti overlay network. Each router has its own identity and must be enrolled with the controller.
|
||||
A Ziti environment requires one or more routers.
|
||||
|
||||
If the Ziti Edge features are enabled, routers may optionally be enrolled as an "edge router". Edge routers allow Ziti
|
||||
SDK enabled applications, Ziti Applications, to access services or host services that have been configured within Ziti
|
||||
as overlay services.
|
||||
|
||||
# Ziti Applications
|
||||
|
||||
Below is an outline of all the applications that are generated from this repository.
|
||||
|
||||
## Servers
|
||||
|
||||
The following binaries are used to deploy long running servers that route traffic and manage the
|
||||
configuration of a Ziti environment.
|
||||
|
||||
| Binary Name | Description|
|
||||
|-------------------| -----------|
|
||||
| ziti-controller | Runs a central server necessary for Ziti environments|
|
||||
| ziti-router | Runs a server capable of ingress'ing and egress'ing Ziti traffic standalone or as a mesh|
|
||||
|
||||
|
||||
## Tools
|
||||
The following binaries provide utility or testing functionality.
|
||||
|
||||
| Binary Name | Description|
|
||||
|-------------------| -----------|
|
||||
| ziti-enroller | Provides enrollment processing features for executables that do not directly support enrollment
|
||||
| ziti-fabric-gw | Provides JSON RCP web service access to Ziti fabric management features
|
||||
| ziti-fabric-test | The Ziti Fabric Toolbox which is used to test deployed fabric components|
|
||||
|
||||
|
||||
## Management
|
||||
|
||||
The following binaries are used to configure and manage a Ziti environment via command line interactions.
|
||||
|
||||
| Binary Name | Description|
|
||||
|-------------------| -----------|
|
||||
| ziti-fabric | Provides command line access to Ziti Fabric management features|
|
||||
| ziti | Provides command line access to Ziti Edge management features|
|
||||
|
||||
# Endpoint Clients
|
||||
The following binaries are Ziti endpoint clients which have the Ziti SDK built into them and can connected to an
|
||||
edge router. Endpoint clients can be application specific or act as a bridge to other applications, hosts, or underlay
|
||||
networks.
|
||||
|
||||
| Binary Name | Description|
|
||||
|-------------------| -----------|
|
||||
| ziti-proxy | Provides the ability to proxy connections across a Ziti|
|
||||
| ziti-tunnel | Provides the ability to intercept traffic to route traffic across Ziti|
|
||||
|
||||
|
||||
All of the above binaries are cross platform compatible, except ziti-tunnel which is currently Linux only.
|
||||
@@ -0,0 +1,194 @@
|
||||
# Overview
|
||||
|
||||
This README aims to allow the setup of a development local environment as quick as possible. It uses predefined
|
||||
configuration files that are maintained with the source code as well as Docker to run ancillary services (such as
|
||||
databases).
|
||||
|
||||
# Dependencies
|
||||
|
||||
- Go 1.12+
|
||||
- PostgresSQL v10+ (if running Ziti Edge)
|
||||
|
||||
|
||||
# Debugging
|
||||
|
||||
This guide can be used to run all of the Ziti applications via command line or in the debugger of an IDE. The only
|
||||
requirement is that the CLI start in/the debuggers current working directory is assumed to be your $GOPATH.
|
||||
|
||||
|
||||
# Get Started
|
||||
|
||||
### Checkout & Build
|
||||
|
||||
Checkout the entire `ziti` repot to a `$GOPATH/src/bitbucket.org/netfoundry` directory.
|
||||
|
||||
Build the entire project and all binaries or just the Ziti Edge portion:
|
||||
|
||||
To install all: ```go install bitbucket.org/netfoundry/ziti/...```
|
||||
|
||||
...which will build and install the binaries into `$GOPATH/bin`. All future `ziti` commands will run binaries from here.
|
||||
|
||||
### Start PostgreSQL
|
||||
|
||||
If Ziti Edge functionality will be used it will be necessary to start a PostgreSQL database. This database can be on
|
||||
any machine, but the default configuration file assume that the database is running on localhost.
|
||||
|
||||
```
|
||||
docker run -it --name local-postgres -e POSTGRES_PASSWORD=ztpassword -p 5432:5432 postgres
|
||||
```
|
||||
|
||||
Please note that the default `ztpassword` exists in the `edge.router.yml` file used below to start a router
|
||||
running with Edge functionality enabled.
|
||||
|
||||
### Starting the Controller
|
||||
|
||||
If you wish to start the controller with the Ziti Fabric and Ziti Edge enabled:
|
||||
|
||||
```
|
||||
ziti-controller run $GOPATH/src/bitbucket.org/netfoundry/ziti/fabric/etc/ctrl.with.edge.yml
|
||||
```
|
||||
|
||||
|
||||
If you wish to start the Ziti Fabric standalone:
|
||||
|
||||
```
|
||||
ziti-controller run $GOPATH/src/bitbucket.org/netfoundry/ziti/fabric/etc/ctrl.yml
|
||||
```
|
||||
|
||||
Please note that if you start the controller without the Ziti Edge enabled, the Ziti SDK, and edge router functionality
|
||||
will not be usable. The controller can be started and stopped both ways without issue. The only requirement is that the
|
||||
PostgrSQL database must be running if the Ziti Edge functionality is enabled.
|
||||
|
||||
|
||||
# Starting Routers
|
||||
|
||||
The Ziti Fabric requires at least one router (fabric router or edge router). There are four predefined configuration files
|
||||
for running routers in `ziti/fabric/etc/` named `001.yml` to `004.yml`.
|
||||
|
||||
Each configuration file refers to certificate and private keys kept in `ziti/fabric/etc/ca/intermediate/certs` and
|
||||
`ziti/fabric/etc/ca/intermediate/private/`. The steps for starting the router is to first register the router then
|
||||
start it.
|
||||
|
||||
Register:
|
||||
|
||||
```
|
||||
ziti-fabric create router $GOPATH\src\bitbucket.org\netfoundry\ziti\fabric\etc\ca\intermediate\certs\XXX-client.cert.pem
|
||||
```
|
||||
|
||||
Where `XXX` is replaced with `001` through `004`.
|
||||
|
||||
Run:
|
||||
|
||||
```
|
||||
ziti-router run $GOPATH/src/bitbucket.org/netfoundry/ziti/fabric/etc/XXX.yml
|
||||
```
|
||||
|
||||
Where `XXX` is replaced with `001` through `004`.
|
||||
|
||||
# Starting Router As An Edge Router
|
||||
|
||||
Edge routers are routers that have the Edge functionality enabled and allow Ziti SDK enabled application to connect to
|
||||
Ziti. Starting an edge eouter requires that the controller be running with the Edge functionality
|
||||
enabled. This requires the use of the `ctrl.with.edge.yml` configuration to run the controller (see example above).
|
||||
|
||||
To start an edge router the Edge REST API will be used to prime the enrollment process and the
|
||||
`ziti-router enroll` command will be used to finalize the process. The enrollment command will handle adding the fabric
|
||||
and edge router entries necessary. Using the `ziti-fabric create router` command should not and can not be run.
|
||||
|
||||
|
||||
There is only one example configuration file for an edge router:
|
||||
`ziti/fabric/etc/edge.router.yml`. Additional configuration files can be created by copying and altering the
|
||||
file. Specifically the identity section needs to point to unique file locations that do not collide with other identity
|
||||
file and the listening ports need to not be in use.
|
||||
|
||||
### Authenticate w/ the Ziti Edge API
|
||||
|
||||
```
|
||||
POST /authenticate?method=password
|
||||
{
|
||||
"username": "admin",
|
||||
"password": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
Authentication will return a session token that should be supplied either as a cookie (also returned) or a HTTP header
|
||||
called `zt-session`. All subsequent requests will either need to set the HTTP set `zt-session` or provide the cookie in
|
||||
every request.
|
||||
|
||||
|
||||
### Create a Cluster / List Clusters
|
||||
|
||||
A `cluster id` is necessary to create an edge router. A cluster must be created or if one already exists, it can be used.
|
||||
|
||||
List:
|
||||
|
||||
```
|
||||
GET /clusters
|
||||
```
|
||||
|
||||
Create:
|
||||
|
||||
```
|
||||
POST /clusters
|
||||
{
|
||||
"name": "My Cluster",
|
||||
}
|
||||
```
|
||||
|
||||
### Create A Cluster
|
||||
|
||||
```
|
||||
POST /clusters
|
||||
{
|
||||
"name": "My Cluster",
|
||||
}
|
||||
```
|
||||
|
||||
A `clustet id` will be provided in the response as `id`. Retain for later use.
|
||||
|
||||
### Create An Edge Router
|
||||
```
|
||||
POST /edge-routers
|
||||
{
|
||||
"name": "My Edge Router",
|
||||
"clusterId": "<cluster id>"
|
||||
}
|
||||
```
|
||||
|
||||
...where `cluster id` is replaced with a valid cluster id that already exists or was created. It can be re-retrieved by
|
||||
listing the existing clusters via `GET /clusers`. The response from creating an edge router should contain an id in
|
||||
the `id` field. Retain for later use.
|
||||
|
||||
### Retrieve the Edge Router Enrollment Token
|
||||
|
||||
```
|
||||
GET /edge-routers/<id>
|
||||
```
|
||||
|
||||
...where `id` is provided in the response to creating an edge router. It can be re-retrieved by listing the existing
|
||||
edge routers via `GET /edge-routers`. The response from retrieving a edge router should contain an enrollment JWT in the
|
||||
`enrollmentJwt` field. Retain the enrollment JWT in a text file named `enrollment.jwt`.
|
||||
|
||||
### Enroll the Edge Router
|
||||
|
||||
```
|
||||
ziti-router enroll --jwt <path to enrollment.jwt> $GOPATH/src/bitbucket.org/netfoundry/ziti/fabric/etc/edge.router.yml
|
||||
```
|
||||
|
||||
...where `path to enrollment.jwt` is the enrollment JWT for the edge router.
|
||||
|
||||
### Start Ziti Edge Router
|
||||
|
||||
```
|
||||
ziti-router run $GOPATH/src/bitbucket.org/netfoundry/ziti/fabric/etc/edge.router.yml
|
||||
```
|
||||
|
||||
# Further Exploration
|
||||
|
||||
At this point the controller should be running with some number of routers running. It is now possible
|
||||
to explore the Ziti Fabric capabilities via the `ziti-fabric` executable.
|
||||
|
||||
If the controller was started with the Edge
|
||||
functionality enabled the Ziti Edge API can be explored. A POSTMAN collection can be found in
|
||||
`ziti/edge/controller/postman` and the Ziti SDK can be found in `ziti/sdk`. Additionally the `ziti-enroller`, `ziti-tunnel` and
|
||||
`ziti-proxy` have the Ziti SDK embedded within them to act as a starting point for using Ziti end-to-end.
|
||||
@@ -0,0 +1,536 @@
|
||||
# Overview
|
||||
|
||||
This document covers how to deploy Ziti from the ground up on a single local machine. After following this guide, it
|
||||
should be possible to extrapolate the setup to multiple machines. This setup includes both the Fabric and optional
|
||||
Edge components:
|
||||
|
||||
- 1x Ziti Controller + Edge
|
||||
- 1x Ziti Edge Router
|
||||
- 1x Ziti Router
|
||||
- 1x Ziti Tunneler
|
||||
- 1x Ziti Service (a demo netcat service)
|
||||
|
||||
It will also include provisioning and running a Ziti Tunneler with a demo netcat service. The services will use Ziti
|
||||
router egress. The full connection will be:
|
||||
|
||||
SDK -> Edge Router -> Router -> Service
|
||||
|
||||
It is also possible to host a service and thus avoid egress through a router:
|
||||
|
||||
Client SDK -> Edge Router -> Routers (0...1) -> Edge Router -> Server SDK
|
||||
|
||||
This document will guide the former.
|
||||
|
||||
For binaries, this guide provides instructions to build them for your local environment. It is also possible to acquire
|
||||
pre-built binaries from the [NetFoundry Artifactory repository](https://netfoundry.jfrog.io/netfoundry/webapp/#/artifacts/browse/tree/General/ziti-release/ziti-all)
|
||||
which does require a NetFoundry login. Extract the contents an use the binaries for your platform - ensuring that they
|
||||
are on your path.
|
||||
|
||||
## Build Requirements
|
||||
If building your own binaries
|
||||
|
||||
- A Golang environment (1.12 or later)
|
||||
- A NetFoundry email address tied to a Bitbucket account with SSH keys
|
||||
|
||||
## Quick steps to a Building Binaries
|
||||
|
||||
It is not possible to simply use `go get bitbucket.org/netfoundry/ziti` as `go get` does not support specifying an
|
||||
identity file (i.e. SSH keys). Instead the repo must first be retrieved by `git clone`.
|
||||
|
||||
### Linux/MacOS
|
||||
|
||||
repo_dir=~/repos/nf
|
||||
mkdir ${repo_dir}
|
||||
cd ${repo_dir}
|
||||
git clone git@bitbucket.org:netfoundry/ziti.git
|
||||
cd ziti
|
||||
export GOPATH=$(pwd)/build
|
||||
go install ./fabric/... ./edge/... ./cli/...
|
||||
|
||||
### Windows
|
||||
|
||||
set repo_dir=%userprofile%\repos\nf
|
||||
mkdir %repo_dir%
|
||||
cd /d %repo_dir%
|
||||
git clone git@bitbucket.org:netfoundry/ziti.git
|
||||
cd /d %repo_dir%\ziti
|
||||
set GOPATH=%cd%\build
|
||||
go install ./fabric/... ./edge/... ./cli/...
|
||||
|
||||
After you have cloned your repository and ensured it can build, change to $GOPATH or
|
||||
%GOPATH% and execute `bin/ziti`. If everything is working properly you should be greeted
|
||||
with the ziti command line help.
|
||||
|
||||
## Setting up your first environment
|
||||
|
||||
Now that you have ziti cloned and compiling, the next step is to get your very first
|
||||
environment running. These steps will bring you through setting up your first environment
|
||||
with ziti. We will:
|
||||
|
||||
* [Requirements](#Requirements)
|
||||
* [Establish Environment Variables](#Establish-Environment-Variables)
|
||||
* [Create A Certificate Authority](#Create A Certificate Authority)
|
||||
* [Configure & Run A Ziti Controller](#Configure-&-Run-A-Ziti-Controller)
|
||||
* [Configure & Run A Ziti Router](#Configure-&-Run-A-Ziti-Router)
|
||||
* [Configure & Run A Ziti Edge Router](#Configure-&-Run-A-Ziti-Edge-Router)
|
||||
* [Configure A Service & Ziti Tunneler](#Configure-A-Service-&-Ziti-Tunneler)
|
||||
* [Configuring a hosted service](#Configuring-A-Hosted-Service)
|
||||
|
||||
## Requirements
|
||||
|
||||
### Required Tooling
|
||||
|
||||
- A bash shell
|
||||
- nc (netcat)
|
||||
|
||||
### A note on Windows
|
||||
These commands all require a running bash shell. Windows users this means you'll need to use
|
||||
WSL, [cygwin](https://www.cygwin.com/), a Linux virtual machine, or some other environment that supports a bash compliant
|
||||
shell. The easiest thing might just be to use the shell that comes with [git bashfor windows](https://gitforwindows.org/). WSL is maturing more and more: [Mintty and WSL](https://github.com/mintty/wsltty).
|
||||
|
||||
Also note that commands for `ziti`, `ziti-fabric`, `ziti-controller`, and `ziti-router` may need to have the `.exe`
|
||||
suffix added into the command provided in this document.
|
||||
|
||||
### Hosts File
|
||||
|
||||
This document assumes that the following hostnames will resolve on your local system:
|
||||
|
||||
- up-and-running-ctrl.ziti.netfoundry.io
|
||||
- up-and-running-er01.ziti.netfoundry.io
|
||||
- up-and-running-r01.ziti.netfoundry.io
|
||||
|
||||
This means that temporarily adding these to your system hosts file (outlined below) is required for this guide.
|
||||
|
||||
- Windows: `windows: %windir%\system32\drivers\etc\hosts`
|
||||
- Linux: `/etc/hosts`
|
||||
|
||||
|
||||
127.0.0.1 up-and-running-ctrl.ziti.netfoundry.io
|
||||
127.0.0.1 up-and-running-er01.ziti.netfoundry.io
|
||||
127.0.0.1 up-and-running-r01.ziti.netfoundry.io
|
||||
|
||||
If you are deploying on multiple machines with their own resolvable hostnames, this is not required. However, it will
|
||||
be necessary to alter the Environment Variable DNS San values to match your environment. Also, all generated Ziti
|
||||
Identities (server, client, and private keys), configuration files, and binaries will need to be copied to the correct
|
||||
hosts.
|
||||
|
||||
## Establish Environment Variables
|
||||
|
||||
The environment variables ZITI_HOME and ZITI_NETWORK will be used to boostrap an environment configuration file and
|
||||
directory structure to hold the various certificate, private keys, configuration files, and enrollment tokens that will
|
||||
be generated in this guide. This will help keep them separated on your file system for easy deletion later.
|
||||
|
||||
After these are defined an environment folder and 'env' file will be generated that can easily reset these values.
|
||||
|
||||
1. Define the boot strap environment variables
|
||||
|
||||
export ZITI_NETWORK=up-and-running
|
||||
export ZITI_HOME=~/.ziti
|
||||
|
||||
For Windows users: if a bash shell is being used some absolute paths may not work (i.e. /mnt/c/myziti). Using paths
|
||||
that start with `~` are generally safer. Using `.` will make configuration files and `env` files sensitive to the
|
||||
current working directory, but should work as long as the correct current working directory is maintained.
|
||||
|
||||
1. Generate the base directory structure and `env` file
|
||||
|
||||
mkdir -p $ZITI_HOME
|
||||
|
||||
echo -e "export ZITI_HOME=${ZITI_HOME}\n\
|
||||
export ZITI_NETWORK=${ZITI_NETWORK}\n\
|
||||
export ZITI_ID=\"\${ZITI_HOME}/identities.yml\"\n\
|
||||
export ZITI_CA_NAME=\"\${ZITI_NETWORK}\"\n\
|
||||
export ZITI_PKI=\"\${ZITI_HOME}/pki\"\n\
|
||||
export ZITI_CA_FILE=\"\${ZITI_PKI}/\${ZITI_CA_NAME}/certs/\${ZITI_CA_NAME}.cert\"\n\
|
||||
export ZITI_CTRL_HOSTNAME=\"\${ZITI_NETWORK}-ctrl.ziti.netfoundry.io\"\n\
|
||||
export ZITI_ER01_HOSTNAME=\"\${ZITI_NETWORK}-er01.ziti.netfoundry.io\"\n\
|
||||
export ZITI_R01_HOSTNAME=\"\${ZITI_NETWORK}-r01.ziti.netfoundry.io\"\n\
|
||||
export ZITI_EDGE_API_PORT=1280\n\
|
||||
export ZITI_EDGE_API_HOSTNAME=\"\${ZITI_CTRL_HOSTNAME}:\${ZITI_EDGE_API_PORT}\"\n\
|
||||
mkdir -p \$ZITI_HOME/db
|
||||
mkdir -p \$ZITI_PKI
|
||||
" > ${ZITI_HOME}/env
|
||||
|
||||
This file generated by the above snippet can be sourced any time to switch between working with multiple environments
|
||||
generated by this document or when starting new terminal windows.
|
||||
|
||||
Source the newly created `env` file: `source ${ZITI_HOME}/env` or if the defaults were used `source .ziti/env`
|
||||
|
||||
- `ZITI_HOME` - The root directory that should be consider the "HOME" directory (defaults to ~ if not set)
|
||||
- `ZITI_NETWORK` - The name of the network being worked with, this value is used as the basis for other file names. Should not contain spaces
|
||||
- `ZITI_PKI` - The location of the PKI used by `ziti pki` via the `--pki-root` option
|
||||
- `ZITI_CA_NAME` - The name of the signing CA that will be used for certificate generation and controller configuration. Defaults to the name of the network.
|
||||
- `ZITI_CA_FILE` - Location of the CA signing cert for ease of use using `ziti` CLI commands
|
||||
- `ZITI_CTRL_HOSTNAME` - DNS SANs for controller certificate generation, should match hosts file changes
|
||||
- `ZITI_ER01_HOSTNAME` - DNS SANs for edge router configuration, should match hosts file changes
|
||||
- `ZITI_R01_HOSTNAME` - DNS SANs for router certificate generation, should match hosts file changes
|
||||
- `ZITI_EDGE_API_PORT` - The port the Edge API will be available on
|
||||
- `ZITI_EDGE_API_HOSTNAME` - The hostname/port combination for the Ziti Edge API, used with `ziti edge controller` commands
|
||||
|
||||
## Create A Certificate Authority
|
||||
|
||||
All communication between Ziti components is secured using mutual TLS. In order to get a ziti environment running a
|
||||
certificate authority is required. This document guides the reader through creating a Root Certificate Authority
|
||||
(Root CA). It does not cover setups that should utilize Intermediate Certificate Authorities for production environments.
|
||||
|
||||
The `ziti` CLI will be significantly streamline and reduce the prerequisite knowledge required to generate
|
||||
a Root CA and signed certificates for client and server usage. Specifically the `ziti pki` command do most of the heavy
|
||||
lifting.
|
||||
|
||||
1. Ensure that the `env` file is sourced if you are using a new terminal.
|
||||
|
||||
1. Create the CA
|
||||
|
||||
ziti pki create ca --pki-root="${ZITI_PKI}" --ca-file="${ZITI_CA_NAME}"
|
||||
|
||||
Additional certificate common name and subject fields can be set as needed. Run `ziti pki create ca -h` for options.
|
||||
|
||||
1. (Optional) Verify the output
|
||||
|
||||
ls -l $ZITI_PKI/$ZITI_CA_NAME
|
||||
|
||||
Example:
|
||||
|
||||
total 0
|
||||
drwxr-xr-x 1 cd cd 512 Jan 28 16:39 certs
|
||||
drwx------ 1 cd cd 512 Jan 28 16:39 crls
|
||||
drwx------ 1 cd cd 512 Jan 28 16:39 keys
|
||||
-rw-r--r-- 1 cd cd 3 Jan 28 16:39 crlnumber
|
||||
-rw-r--r-- 1 cd cd 162 Jan 28 16:39 index.txt
|
||||
-rw-r--r-- 1 cd cd 20 Jan 28 16:39 index.txt.attr
|
||||
-rw-r--r-- 1 cd cd 3 Jan 28 16:39 serial
|
||||
|
||||
|
||||
## Configure-&-Run-A-Ziti-Controller
|
||||
|
||||
### Postgres
|
||||
|
||||
A Postgres installation is required for the Edge components of the Ziti Controller to operate. If an existing Postgres
|
||||
instance is available, then substitute the connection information in the controller configuration that is generated
|
||||
in this section.
|
||||
|
||||
It is suggested that Postgres be installed via Docker; as outlined below.
|
||||
|
||||
If desired, Postgres can be installed on your host machine via your platforms package management system (if available)
|
||||
or directly from https://www.postgresql.org/. The Docker approach is highly encouraged for easy tear down.
|
||||
|
||||
### Configure & Start
|
||||
|
||||
The following steps outline how to configure and start the Ziti Controller.
|
||||
|
||||
1. Ensure that the `env` file is sourced if you are using a new terminal.
|
||||
|
||||
1. Start a Postgres environment on your local machine (either local installation or Docker container, Docker commands provided below:
|
||||
|
||||
- Install & Run: `docker run --name pg -d -e POSTGRES_PASSWORD=ztpassword -p 5432:5432 postgres`
|
||||
- Stop: `docker stop pg`
|
||||
- Start: `docker start pg`
|
||||
- Verify: `docker ps` see the `postgres` image is running under name `pg`
|
||||
|
||||
1. Generate the controller Ziti Identity (server, client, and private key)
|
||||
|
||||
Server
|
||||
|
||||
ziti pki create server --pki-root=$ZITI_PKI --ca-name $ZITI_CA_NAME \
|
||||
--server-file "${ZITI_NETWORK}-ctrl-server" \
|
||||
--dns "${ZITI_CTRL_HOSTNAME}" --ip "127.0.0.1" \
|
||||
--server-name "${ZITI_NETWORK} Controller"
|
||||
|
||||
Client
|
||||
|
||||
ziti pki create client --pki-root=$ZITI_PKI --ca-name ${ZITI_CA_NAME} \
|
||||
--client-file "${ZITI_NETWORK}-ctrl-client" \
|
||||
--key-file "${ZITI_NETWORK}-ctrl-server" \
|
||||
--client-name "${ZITI_NETWORK} Controller"
|
||||
|
||||
Note: The client cert uses the controller server key as specified by `--key-file`.
|
||||
|
||||
Additional certificate common name and subject fields can be set as needed. Run `ziti pki create server -h` or
|
||||
`ziti pki create client -h` for options.
|
||||
|
||||
The Ziti Identity files are output in the following locations
|
||||
|
||||
$ZITI_PKI/$ZITI_CA_NAME/certs/${ZITI_NETWORK}-ctrl-server.cert
|
||||
$ZITI_PKI/$ZITI_CA_NAME/certs/${ZITI_NETWORK}-ctrl-client.cert
|
||||
$ZITI_PKI/$ZITI_CA_NAME/keys/${ZITI_NETWORK}-ctrl-server.key
|
||||
|
||||
|
||||
1. Generate the controller configuration file
|
||||
|
||||
echo "
|
||||
v: 3
|
||||
db: $ZITI_HOME/db/ctrl.db
|
||||
|
||||
identity:
|
||||
cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-ctrl-client.cert
|
||||
server_cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-ctrl-server.cert
|
||||
key: ${ZITI_PKI}/${ZITI_CA_NAME}/keys/${ZITI_NETWORK}-ctrl-server.key
|
||||
ca: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}.cert
|
||||
|
||||
ctrl:
|
||||
listener: tls:127.0.0.1:6262
|
||||
|
||||
mgmt:
|
||||
listener: tls:127.0.0.1:10000
|
||||
edge:
|
||||
api:
|
||||
listener: 0.0.0.0:1280
|
||||
advertise: ${ZITI_EDGE_API_HOSTNAME}
|
||||
sessionTimeoutMinutes: 30
|
||||
enrollment:
|
||||
signingCert:
|
||||
cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}.cert
|
||||
key: ${ZITI_PKI}/${ZITI_CA_NAME}/keys/${ZITI_NETWORK}.key
|
||||
edgeIdentity:
|
||||
durationMinutes: 5
|
||||
edgeRouter:
|
||||
durationMinutes: 5
|
||||
persistence:
|
||||
connectionUrl: postgres://postgres:ztpassword@localhost/postgres?sslmode=disable
|
||||
dbName: postgres
|
||||
" > $ZITI_HOME/controller.yaml
|
||||
|
||||
1. Initialize the database if this is the first time you are going to start the controller
|
||||
|
||||
# if needed, initialize the controller
|
||||
ziti-controller edge init $ZITI_HOME/controller.yaml -u admin -p admin
|
||||
|
||||
# run the controller
|
||||
ziti-controller run $ZITI_HOME/controller.yaml
|
||||
|
||||
Note that running this command will commandeer your current terminal. Use `screen` or multiple terminals by using
|
||||
`source` on the `env` file.
|
||||
|
||||
## Configure & Run A Ziti Router
|
||||
|
||||
This section outlines the steps necessary to configure and connect a router to the Ziti Controller. The router will be
|
||||
named `r01` which is represented in the Common Name field of the certificates for the router. Routers join a controller
|
||||
and create a mesh to provide long-haul transport.
|
||||
|
||||
To enroll a Ziti Router, the command line utility `ziti-fabric` will be used that requires its own Ziti Identity in order
|
||||
to connect to and control the fabric.
|
||||
|
||||
To configure more routers, repeat steps 3+ from below but be sure to
|
||||
- alter the `--server-name`, `--client-name`, and `--dns` parameters in all commands
|
||||
- update the hosts file with any new hostnames
|
||||
|
||||
1. The `ziti-fabric` command will be used to manage the fabric, a Ziti Identity must be generated to do that:
|
||||
|
||||
|
||||
ziti pki create client --pki-root="${ZITI_PKI}" --ca-name="${ZITI_CA_NAME}" \
|
||||
--client-file="${ZITI_NETWORK}-dotzeet" \
|
||||
--client-name "${ZITI_NETWORK} Management"
|
||||
|
||||
1. Generate an `identities.json` file that references the Ziti Identity:
|
||||
|
||||
echo "
|
||||
---
|
||||
default:
|
||||
caCert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}.cert
|
||||
cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-dotzeet.cert
|
||||
key: ${ZITI_PKI}/${ZITI_CA_NAME}/keys/${ZITI_NETWORK}-dotzeet.key
|
||||
endpoint: tls:127.0.0.1:10000
|
||||
" > $ZITI_HOME/identities.yml
|
||||
|
||||
1. Verify the identity works:
|
||||
|
||||
ziti-fabric list routers
|
||||
|
||||
Note: If 127.0.0.1 was not included in the controller cert, use the `-e` flag to set the address to the controller
|
||||
(`ziti-fabric -e "tls:up-and-running-ctrl.ziti.netfoundry.io:10000" list routers` ) on this an all future
|
||||
`ziti-fabric` command.
|
||||
|
||||
1. Generate the Ziti Router identity
|
||||
|
||||
Server
|
||||
|
||||
ziti pki create server --pki-root=$ZITI_PKI --ca-name ${ZITI_CA_NAME} \
|
||||
--server-file "${ZITI_NETWORK}-r01-server" \
|
||||
--dns "${ZITI_R01_HOSTNAME}" --ip 127.0.0.1 \
|
||||
--server-name r01
|
||||
|
||||
Client
|
||||
|
||||
ziti pki create client --pki-root=$ZITI_PKI --ca-name ${ZITI_CA_NAME} \
|
||||
--client-file "${ZITI_NETWORK}-r01-client" \
|
||||
--key-file "${ZITI_NETWORK}-r01-server" \
|
||||
--client-name r01
|
||||
|
||||
1. Generate the router configuration file:
|
||||
|
||||
echo "v: 2
|
||||
|
||||
identity:
|
||||
cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-r01-client.cert
|
||||
server_cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-r01-server.cert
|
||||
key: ${ZITI_PKI}/${ZITI_CA_NAME}/keys/${ZITI_NETWORK}-r01-server.key
|
||||
ca: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-r01-server.chain.pem
|
||||
|
||||
ctrl:
|
||||
endpoint: tls:${ZITI_CTRL_HOSTNAME}:6262
|
||||
|
||||
link:
|
||||
listener: quic:127.0.0.1:6002
|
||||
|
||||
listeners:
|
||||
- binding: transport
|
||||
address: tls:0.0.0.0:7002
|
||||
" > $ZITI_HOME/r01.yaml
|
||||
|
||||
1. Enroll the Ziti Router identity with the controller
|
||||
|
||||
ziti-fabric create router "${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-r01-client.cert"
|
||||
|
||||
Verify:
|
||||
|
||||
ziti-fabric list routers
|
||||
|
||||
Should show 1 router named `r01`.
|
||||
|
||||
1. Start the router:
|
||||
|
||||
ziti-router run $ZITI_HOME/r01.yaml
|
||||
|
||||
Note that running this command will commandeer your current terminal. Use `screen` or multiple terminals by using
|
||||
`source` on the `env` file.
|
||||
|
||||
|
||||
## Configure & Run A Ziti Edge Router
|
||||
|
||||
A Ziti Edge Router allows the Ziti SDK to ingress to a Ziti overlay network. A Ziti Edge Router is a Ziti Router that has the `edge`
|
||||
section of the configuration defined and has gone through the Edge Router Enrollment process instead of the Ziti Router
|
||||
enrollment process. Ziti Edge Routers and Ziti Routers are run from the same binary `ziti-router`.
|
||||
|
||||
Ensure that the `env` file is sourced if you are using a new terminal.
|
||||
|
||||
1. Authenticate w/ the controller if not authenticated
|
||||
|
||||
#update username / password if the default admin password has been updated
|
||||
ziti edge controller login ${ZITI_EDGE_API_HOSTNAME} -u admin -p admin -c ${ZITI_CA_FILE}
|
||||
|
||||
1. Create a cluster
|
||||
|
||||
ziti edge controller create cluster cluster01
|
||||
|
||||
1. Create a edge router and output the JWT to a file:
|
||||
|
||||
ziti edge controller create edge-router er01 cluster01 -o $ZITI_HOME/er01.jwt
|
||||
|
||||
1. Create the edge router configuration file
|
||||
|
||||
echo "
|
||||
v: 2
|
||||
|
||||
identity:
|
||||
cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-er01-client.cert
|
||||
server_cert: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-er01-server.cert
|
||||
key: ${ZITI_PKI}/${ZITI_CA_NAME}/keys/${ZITI_NETWORK}-er01-server.key
|
||||
ca: ${ZITI_PKI}/${ZITI_CA_NAME}/certs/${ZITI_NETWORK}-er01-server.chain.cert
|
||||
|
||||
ctrl:
|
||||
endpoint: tls:${ZITI_CTRL_HOSTNAME}:6262
|
||||
|
||||
edge:
|
||||
csr:
|
||||
country: US
|
||||
province: NC
|
||||
locality: Charlotte
|
||||
organization: NetFoundry
|
||||
organizationalUnit: Ziti
|
||||
sans:
|
||||
dns:
|
||||
- ${ZITI_NETWORK}-er01.ziti.netfoundry.io
|
||||
ip:
|
||||
- "127.0.0.1"
|
||||
|
||||
dialers:
|
||||
- binding: udp
|
||||
- binding: transport
|
||||
|
||||
listeners:
|
||||
- binding: edge
|
||||
address: tls:0.0.0.0:3022
|
||||
options:
|
||||
advertise: ${ZITI_NETWORK}-er01.ziti.netfoundry.io:3022
|
||||
- binding: transport
|
||||
address: tls:0.0.0.0:7099
|
||||
" > $ZITI_HOME/er01.yaml
|
||||
|
||||
1. Complete the enrollment process with the controller
|
||||
|
||||
ziti-router enroll $ZITI_HOME/er01.yaml --jwt $ZITI_HOME/er01.jwt
|
||||
|
||||
1. Start the edge router:
|
||||
|
||||
ziti-router run $ZITI_HOME/er01.yaml
|
||||
|
||||
Note that running this command will commandeer your current terminal. Use `screen` or multiple terminals by using
|
||||
`source` on the `env` file.
|
||||
|
||||
|
||||
## Configure A Service & Ziti Tunneler
|
||||
|
||||
1. Authenticate w/ the controller if not authenticated
|
||||
|
||||
#update username / password if the default admin password has been updated
|
||||
ziti edge controller login ${ZITI_EDGE_API_HOSTNAME} -u admin -p admin -c ${ZITI_CA_FILE}
|
||||
|
||||
1. Create a service that will facilitate connecting to a local netcat server listening on port 7256 and that egresses
|
||||
the Ziti Fabric on our "r01" router
|
||||
|
||||
ziti edge controller create service netcat7256 localhost 7256 r01 tcp://localhost:7256
|
||||
|
||||
|
||||
1. Create a Ziti Edge Identity for the Ziti Tunneler process
|
||||
|
||||
ziti edge controller create identity device identity01 -o $ZITI_HOME/identity01.jwt
|
||||
|
||||
1. Create an AppWan to associate the Ziti Tunneler identity (identity01) to the service (netcat7256)
|
||||
|
||||
ziti edge controller create app-wan appwan01 -s netcat7256 -i identity01
|
||||
|
||||
1. Enroll the Ziti Tunneler's identity
|
||||
|
||||
ziti-enroller --jwt $ZITI_HOME/identity01.jwt -o $ZITI_HOME/identity01.json
|
||||
|
||||
1. Start the Ziti Tunneler in proxy mode
|
||||
|
||||
ziti-tunnel proxy netcat7256:8145 -i $ZITI_HOME/identity01.json
|
||||
|
||||
1. Start the netcat server
|
||||
|
||||
nc -k -n 127.0.0.1 -l 7256
|
||||
|
||||
1. Start the netcat client that will connect to the Ziti Tunnel proxy
|
||||
|
||||
nc -v 127.0.0.1 8145
|
||||
|
||||
|
||||
## Configuring A Hosted Service
|
||||
|
||||
Services can also be hosted by another SDK. For this to work a second Ziti Tunneler can be setup to act as the host. The
|
||||
exact details of this are beyond this document but the high level steps are outlined below:
|
||||
|
||||
1. Create a new Ziti Identity via `ziti edge controller identity create` to allow another Ziti Tunneler to run
|
||||
1. Enroll the Ziti Identitiy via `ziti-enroller`
|
||||
1. Add the new Ziti Identity to the hosts of a new service or an existing service
|
||||
1. Start the new Ziti Tunneler
|
||||
|
||||
|
||||
|
||||
Example Hosted Service
|
||||
|
||||
```sh
|
||||
ziti edge controller create service <service name> <dns host> <dns port> \
|
||||
--hosted
|
||||
--hosted-ids <someIdentityId>
|
||||
--tags tunneler.dial.addr=<address for tunneler to dial>
|
||||
```
|
||||
|
||||
Example:
|
||||
````
|
||||
ziti edge controller create service postgresql pg 5432 \-
|
||||
--hosted
|
||||
--hosted-ids 40c025cb-bc92-4a54-b55f-1429412f2644
|
||||
-tags tunneler.dial.addr=tcp:127.0.0.1:5432
|
||||
```
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
|
||||
|
||||
This page discusses the changes that you need to be aware of when migrating your Ziti deployment from version 0.5.x to version 0.6.x
|
||||
|
||||
# Theme
|
||||
Ziti 0.6.0 move the back-end persistence model of Ziti Edge and Ziti Fabric into the same repository based on Bbolt (an in memory data store that is backed by a memory mapped file). The changes remove the requirement for PostgresSQL.
|
||||
|
||||
# UPDB Enrollment JWTs
|
||||
Enrollments that are for UPDB (username password database) are now consistent with all other enrollment and use JWTs for processing. Prior to this a naked URL was provided.
|
||||
|
||||
## What This Breaks
|
||||
Any UPDB enrollment processing that relied upon the URL for the enrollment.
|
||||
|
||||
|
||||
|
||||
Ziti 0.5.x UPDB enrolling entity
|
||||
|
||||
```
|
||||
{
|
||||
"meta": {},
|
||||
"data": {
|
||||
"id": "612843ae-6ac8-48ac-a737-bfc2d28ab9ea",
|
||||
"createdAt": "2019-11-21T17:23:00.316631Z",
|
||||
"updatedAt": "2019-11-21T17:23:00.316631Z",
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "./identities/612843ae-6ac8-48ac-a737-bfc2d28ab9ea"
|
||||
}
|
||||
},
|
||||
"tags": {},
|
||||
"name": "updb--5badbdc5-e8dd-4877-82df-c06aea7f1197",
|
||||
"type": {
|
||||
"id": "577104f2-1e3a-4947-a927-7383baefbc9a",
|
||||
"name": "User"
|
||||
},
|
||||
"isDefaultAdmin": false,
|
||||
"isAdmin": false,
|
||||
"authenticators": {},
|
||||
"enrollment": {
|
||||
"updb": {
|
||||
"username": "asdf",
|
||||
"url": "https://demo.ziti.netfoundry.io:1080/enroll?method=updb&token=911e6562-0c83-11ea-a81a-000d3a1b4b17&username=asdf"
|
||||
}
|
||||
},
|
||||
"permissions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Ziti 0.6.x UPDB enrolling entity (note the changes in the enrollment.updb object):
|
||||
|
||||
```
|
||||
{
|
||||
"meta": {},
|
||||
"data": {
|
||||
"id": "39f11c10-0693-41ed-9bec-8011e2721562",
|
||||
"createdAt": "2019-11-21T17:28:18.2855234Z",
|
||||
"updatedAt": "2019-11-21T17:28:18.2855234Z",
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "./identities/39f11c10-0693-41ed-9bec-8011e2721562"
|
||||
}
|
||||
},
|
||||
"tags": {},
|
||||
"name": "updb--b55f5372-3993-40f5-b534-126e0dd2f1be",
|
||||
"type": {
|
||||
"entity": "identity-types",
|
||||
"id": "577104f2-1e3a-4947-a927-7383baefbc9a",
|
||||
"name": "User",
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "./identity-types/577104f2-1e3a-4947-a927-7383baefbc9a"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDefaultAdmin": false,
|
||||
"isAdmin": false,
|
||||
"authenticators": {},
|
||||
"enrollment": {
|
||||
"updb": {
|
||||
"expiresAt": "2019-11-21T17:33:18.2855234Z",
|
||||
"issuedAt": "2019-11-21T17:28:18.2855234Z",
|
||||
"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbSI6InVwZGIiLCJleHAiOjE1NzQzNTc1OTgsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0OjEyODAiLCJqdGkiOiJiYzBlY2NlOC05ZGY0LTQzZDYtYTVhMC0wMjI1MzY2YmM4M2EiLCJzdWIiOiIzOWYxMWMxMC0wNjkzLTQxZWQtOWJlYy04MDExZTI3MjE1NjIifQ.PUcnACCdwqfWRGRzF8lG6xDTgHKAwKV6eTw8tHFuNBXaUNbqExBwUQEW0-cCHsV-nLEyhxyjhXmVCkIDgz-ukKfS0xStiDrJQbiq8m0auodkArmJSsYzElXkKdv37FHu0t-CGoXptdLyuo9eCnzzmci3ev18zMR5HjYMCQEclELV6OEICNr_0EwhAGJa1yX6ODYrLMZ3SdEd6fj-ZGX7j9owTs6iEsqCB_TORfnGGg6lEINE5GlYsyp7JUxolS6H4lPeN5h2mxk2_OkJY8GX3ydv75LsIZ-jjL3xC5XncCESrefgDabib1fudJ4038D0EzqTcOREPAqmjWhnDhTulQ",
|
||||
"token": "bc0ecce8-9df4-43d6-a5a0-0225366bc83a"
|
||||
}
|
||||
},
|
||||
"permissions": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## What To Do
|
||||
Use the new JWT format to:
|
||||
|
||||
verify the signature of the JWT to match the iss URL's TSL presented certificates
|
||||
construct the enrollment url from the JWTs properties in the following format:
|
||||
|
||||
```
|
||||
<iss> + "/enroll?token=" + <jti>
|
||||
```
|
||||
|
||||
# Multiple Invalid Value Error Handling
|
||||
Errors where there is the potential to report about multiple invalid field values for a given field used to report as a separate error for each value. Now there will be one error, but the values field will hold the invalid values.
|
||||
|
||||
## Old Format
|
||||
```
|
||||
{
|
||||
"error": {
|
||||
"args": {
|
||||
"urlVars": {
|
||||
"id": "097018b6-108e-42b3-869b-deb9e1814594"
|
||||
}
|
||||
},
|
||||
"cause": {
|
||||
"errors": [
|
||||
{
|
||||
"message": "entity not found for id [06ecf930-3a9f-4a6c-98b5-8f0be1bde9e2]",
|
||||
"field": "ids[0]",
|
||||
"value": "06ecf930-3a9f-4a6c-98b5-8f0be1bde9e2"
|
||||
}
|
||||
]
|
||||
},
|
||||
"causeMessage": "There were multiple field errors: the value '06ecf930-3a9f-4a6c-9...' for 'ids[0]' is invalid: entity not found for id [06ecf930-3a9f-4a6c-98b5-8f0be1bde9e2]",
|
||||
"code": "INVALID_FIELD",
|
||||
"message": "The field contains an invalid value",
|
||||
"requestId": "48ea4bce-f233-410e-a062-5dbceee20223"
|
||||
},
|
||||
"meta": {
|
||||
"apiEnrolmentVersion": "0.0.1",
|
||||
"apiVersion": "0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## New Format
|
||||
```
|
||||
{
|
||||
"error": {
|
||||
"args": {
|
||||
"urlVars": {
|
||||
"id": "5b15c442-5590-4c58-8bc7-0da788e0cfcf"
|
||||
}
|
||||
},
|
||||
"cause": {
|
||||
"message": "clusters(s) not found",
|
||||
"field": "clusters",
|
||||
"value": [
|
||||
"68f8739f-cf52-4d51-9553-dfe7cf9c6a03"
|
||||
]
|
||||
},
|
||||
"causeMessage": "the value '[68f8739f-cf52-4d51-9553-dfe7cf9c6a03]' for 'clusters' is invalid: clusters(s) not found",
|
||||
"code": "INVALID_FIELD",
|
||||
"message": "The field contains an invalid value",
|
||||
"requestId": "ab6553e8-e9b1-408c-9fed-11cc627cfd84"
|
||||
},
|
||||
"meta": {
|
||||
"apiEnrolmentVersion": "0.0.1",
|
||||
"apiVersion": "0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ZitiDialContext struct {
|
||||
context ziti.Context
|
||||
}
|
||||
|
||||
func (dc *ZitiDialContext) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
service := strings.Split(addr, ":")[0] // will always get passed host:port
|
||||
return dc.context.Dial(service)
|
||||
}
|
||||
|
||||
func newZitiClient() *http.Client {
|
||||
zitiDialContext := ZitiDialContext{context: ziti.NewContext()}
|
||||
zitiTransport := *http.DefaultTransport.(*http.Transport) // copy default transport
|
||||
zitiTransport.DialContext = zitiDialContext.Dial
|
||||
return &http.Client{Transport: &zitiTransport}
|
||||
}
|
||||
|
||||
func main() {
|
||||
resp, err := newZitiClient().Get(os.Args[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = io.Copy(os.Stdout, resp.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Greeter string
|
||||
|
||||
func (g Greeter) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
|
||||
var result string
|
||||
if name := req.URL.Query().Get("name"); name != "" {
|
||||
result = fmt.Sprintf("Hello, %v, from %v\n", name, g)
|
||||
} else {
|
||||
result = "Who are you?\n"
|
||||
}
|
||||
if _, err := resp.Write([]byte(result)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func serve(listener net.Listener, serverType string) {
|
||||
if err := http.Serve(listener, Greeter(serverType)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func plain(listenAddr string) {
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
serve(listener, "plain")
|
||||
}
|
||||
|
||||
func withZiti(service string) {
|
||||
listener, err := ziti.NewContext().Listen(service)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
serve(listener, "ziti")
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) > 2 && os.Args[1] == "ziti" {
|
||||
withZiti(os.Args[2])
|
||||
} else {
|
||||
plain("localhost:8080")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
module github.com/netfoundry/ziti-edge
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2
|
||||
github.com/coreos/go-iptables v0.4.3
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible
|
||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/mdlayher/netlink v0.0.0-20191009155606-de872b0d824b
|
||||
github.com/michaelquigley/pfxlog v0.0.0-20190813191113-2be43bd0dccc
|
||||
github.com/miekg/dns v1.1.22
|
||||
github.com/mitchellh/mapstructure v1.1.2
|
||||
github.com/netfoundry/ziti-foundation v0.0.0-20191203170231-5c1204e0e65d
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/spf13/cobra v0.0.5
|
||||
github.com/stretchr/testify v1.2.2
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9
|
||||
)
|
||||
@@ -0,0 +1,135 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2 h1:NYoPVh1XuUB5VBWLXRKoqzQhl4bajIxh+XuURbJ0uwc=
|
||||
github.com/achanda/go-sysctl v0.0.0-20160222034550-6be7678c45d2/go.mod h1:DCNKSpXhum14Y258jSbRmJvcesbzEdBPincz7yJUx3k=
|
||||
github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE=
|
||||
github.com/antlr/antlr4 v0.0.0-20191115170859-54daca92f7b0/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/biogo/store v0.0.0-20190426020002-884f370e325d/go.mod h1:Iev9Q3MErcn+w3UOJD/DkEzllvugfdx7bGcMOFhvr/4=
|
||||
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-iptables v0.4.3 h1:jJg1aFuhCqWbgBl1VTqgTHG5faPM60A5JDMjQ2HYv+A=
|
||||
github.com/coreos/go-iptables v0.4.3/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
|
||||
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa h1:RDBNVkRviHZtvDvId8XSGPu3rmpmSe+wKRcEWNgsfWU=
|
||||
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0=
|
||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/kataras/go-events v0.0.3-0.20170604004442-17d67be645c3/go.mod h1:6IxMW59VJdEIqj3bjFGJvGLRdb0WHtrlxPZy9qXctcg=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/lucas-clemente/quic-go v0.13.1/go.mod h1:Vn3/Fb0/77b02SGhQk36KzOUmXgVpFfizUfW5WMaqyU=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/marten-seemann/chacha20 v0.2.0/go.mod h1:HSdjFau7GzYRj+ahFNwsO3ouVJr1HFkWoEwNDb4TMtE=
|
||||
github.com/marten-seemann/qpack v0.1.0/go.mod h1:LFt1NU/Ptjip0C2CPkhimBz5CGE3WGDAUWqna+CNTrI=
|
||||
github.com/marten-seemann/qtls v0.4.1/go.mod h1:pxVXcHHw1pNIt8Qo0pwSYQEoZ8yYOOPXTCZLQQunvRc=
|
||||
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v0.0.0-20191009155606-de872b0d824b h1:W3er9pI7mt2gOqOWzwvx20iJ8Akiqz1mUMTxU6wdvl8=
|
||||
github.com/mdlayher/netlink v0.0.0-20191009155606-de872b0d824b/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/michaelquigley/pfxlog v0.0.0-20190813191113-2be43bd0dccc h1:PQrqw9XKYcPRI9rKwoGZucMjWvfZpyfb5j8Lc13j8YY=
|
||||
github.com/michaelquigley/pfxlog v0.0.0-20190813191113-2be43bd0dccc/go.mod h1:Z0K0l/NScHvLeGoy4SZTps+ZxGv85ql4AvZqTqOxBY8=
|
||||
github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc=
|
||||
github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
|
||||
github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw=
|
||||
github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/netfoundry/ziti-foundation v0.0.0-20191203170231-5c1204e0e65d h1:V6VZeV8JVUipJhZgDUNUhaJ2L6bqmosAf+SNUj6C1f0=
|
||||
github.com/netfoundry/ziti-foundation v0.0.0-20191203170231-5c1204e0e65d/go.mod h1:27kvN9RZk7cpt2xba6owuXktNphq5hPK62CAiKnIS10=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708 h1:pXVtWnwHkrWD9ru3sDxY/qFK/bfc0egRovX91EjWjf4=
|
||||
golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190228165749-92fc7df08ae7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU=
|
||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -0,0 +1,38 @@
|
||||
Ziti SDK for Go
|
||||
---------------
|
||||
|
||||
# Enrollment
|
||||
Prerequisite: Ziti Enrollment token in JWT format (e.g. `device.jwt`)
|
||||
|
||||
Run enrollment process to generate SDK configuration file -- `device.json`
|
||||
```
|
||||
$ ziti-enroller -jwt device.jwt -out device.json
|
||||
```
|
||||
|
||||
----
|
||||
|
||||
Note: additional options (`-cert`, `-key`, `-engine`)
|
||||
are available to enroll with `ottCa` and `CA` methods
|
||||
---
|
||||
|
||||
# Using SDK
|
||||
|
||||
SDK is using `ZITI_SDK_CONFIG` environment variable to load configuration file.
|
||||
|
||||
# Using `ziti-proxy`
|
||||
|
||||
`ziti-proxy` opens up local tcp port for proxy-ing to the target service.
|
||||
Services are identified by names in Ziti Controlller. It supports multiple services.
|
||||
Usage:
|
||||
```
|
||||
$ ziti-proxy <service-name1>:<port1> [<service-name2>:<port2>]*
|
||||
```
|
||||
`ZITI_SDK_CONFIG` enviroment has to be set.
|
||||
|
||||
Example:
|
||||
```
|
||||
$ export ZITI_SDK_CONFIG=device.json
|
||||
$ ziti-proxy netcat:33169
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/cmd/ziti-enroller/subcmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
subcmd.Execute()
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package subcmd
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/identity/certtools"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/enroll"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// global state used by all subcommands are located here for easy discovery
|
||||
var verbose bool
|
||||
var jwtpath, outpath, keyPath, certPath, idname, caOverride string
|
||||
|
||||
const verboseDesc = "Enable verbose logging."
|
||||
const outpathDesc = "Output configuration file."
|
||||
const jwtpathDesc = "Enrollment token (JWT file). Required"
|
||||
const certDesc = "The certificate to present when establishing a connection."
|
||||
const idnameDesc = "Names the identity. Ignored if not 3rd party auto enrollment"
|
||||
|
||||
const outFlag = "out"
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, verboseDesc)
|
||||
rootCmd.Flags().StringVarP(&jwtpath, "jwt", "j", "", jwtpathDesc)
|
||||
rootCmd.Flags().StringVarP(&outpath, outFlag, "o", "", outpathDesc)
|
||||
rootCmd.Flags().StringVarP(&idname, "idname", "n", "", idnameDesc)
|
||||
rootCmd.Flags().StringVarP(&certPath, "cert", "c", "", certDesc)
|
||||
rootCmd.Flags().StringVarP(&caOverride, "ca", "", "", "Additional trusted certificates")
|
||||
|
||||
var keyDesc = ""
|
||||
engines := certtools.ListEngines()
|
||||
if len(engines) > 0 {
|
||||
keyDesc = fmt.Sprintf("The key to use with the certifcate. Optionally specify the engine to use. supported engines: %v", engines)
|
||||
} else {
|
||||
keyDesc = fmt.Sprintf("The key to use with the certifcate.")
|
||||
}
|
||||
|
||||
rootCmd.Flags().StringVarP(&keyPath, "key", "k", "", keyDesc)
|
||||
|
||||
_ = rootCmd.MarkFlagRequired("jwt")
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ziti-enroller",
|
||||
Short: "Ziti Enroller",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if verbose {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return processEnrollment()
|
||||
},
|
||||
}
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
fmt.Printf("error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func processEnrollment() error {
|
||||
if strings.TrimSpace(outpath) == "" {
|
||||
out, outErr := outPathFromJwt(jwtpath)
|
||||
if outErr != nil {
|
||||
return fmt.Errorf("could not set the output path: %s", outErr)
|
||||
}
|
||||
outpath = out
|
||||
}
|
||||
|
||||
if jwtpath != "" {
|
||||
if _, err := os.Stat(jwtpath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("the provided jwt file does not exist: %s", jwtpath)
|
||||
}
|
||||
}
|
||||
|
||||
if caOverride != "" {
|
||||
if _, err := os.Stat(caOverride); os.IsNotExist(err) {
|
||||
return fmt.Errorf("the provided ca file does not exist: %s", caOverride)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(outpath) == strings.TrimSpace(jwtpath) {
|
||||
return fmt.Errorf("the output path must not be the same as the jwt path")
|
||||
}
|
||||
|
||||
tokenStr, _ := ioutil.ReadFile(jwtpath)
|
||||
|
||||
pfxlog.Logger().Debugf("jwt to parse: %s", tokenStr)
|
||||
tkn, _, err := enroll.ParseToken(string(tokenStr))
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse JWT: %s", err.Error())
|
||||
}
|
||||
|
||||
flags := enroll.EnrollmentFlags{
|
||||
CertFile: certPath,
|
||||
KeyFile: keyPath,
|
||||
Token: tkn,
|
||||
IDName: idname,
|
||||
AdditionalCAs: caOverride,
|
||||
}
|
||||
conf, err := enroll.Enroll(flags)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to enroll: %v", err)
|
||||
}
|
||||
|
||||
output, err := os.Create(outpath)
|
||||
defer output.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file '%s': %s", outpath, err.Error())
|
||||
}
|
||||
|
||||
encErr := json.NewEncoder(output).Encode(&conf)
|
||||
if encErr == nil {
|
||||
fmt.Printf("enrolled successfully. identity file written to: %s", outpath)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("enrollment successful but the identity file was not able to be written to: %s [%s]", outpath, encErr)
|
||||
}
|
||||
}
|
||||
|
||||
func outPathFromJwt(jwt string) (string, error) {
|
||||
if strings.HasSuffix(jwt, ".jwt") {
|
||||
return jwt[:len(jwt)-len(".jwt")] + ".json", nil
|
||||
} else if strings.HasSuffix(jwt, ".json") {
|
||||
//ugh - so that makes things a bit uglier but ok fine. we'll return an error in this situation
|
||||
return "", errors.Errorf("unexpected configuration. cannot infer '%s' flag if the jwt file "+
|
||||
"ends in .json. rename jwt file or provide the '%s' flag", outFlag, outFlag)
|
||||
} else {
|
||||
//doesn't end with .jwt - so just slap a .json on the end and call it a day
|
||||
return jwt + ".json", nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package subcmd
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/common/version"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show component version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(version.GetBuildMetadata(verbose))
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/cmd/ziti-proxy/subcmd"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func init() {
|
||||
pfxlog.Global(logrus.InfoLevel)
|
||||
pfxlog.SetPrefix("bitbucket.org/netfoundry/")
|
||||
}
|
||||
|
||||
func main() {
|
||||
subcmd.Execute()
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package subcmd
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var log = logrus.StandardLogger()
|
||||
|
||||
var runCmd = &cobra.Command{
|
||||
Use: "run <service-name:port> [sevice-name:port]",
|
||||
Short: "Run ziti-proxy",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Run: runFunc,
|
||||
}
|
||||
|
||||
var context ziti.Context
|
||||
|
||||
func init() {
|
||||
root.AddCommand(runCmd)
|
||||
}
|
||||
|
||||
func runFunc(cmd *cobra.Command, args []string) {
|
||||
log.Info(args)
|
||||
closeCh := make(chan interface{})
|
||||
count := 0
|
||||
|
||||
context = ziti.NewContext()
|
||||
|
||||
for _, arg := range args {
|
||||
count = count + 1
|
||||
go runServiceListener(arg, closeCh)
|
||||
}
|
||||
|
||||
for count > 0 {
|
||||
res := <-closeCh
|
||||
|
||||
err, ok := res.(error)
|
||||
if ok {
|
||||
logrus.Error(err)
|
||||
}
|
||||
count = count - 1
|
||||
}
|
||||
}
|
||||
|
||||
func runServiceListener(arg string, close chan interface{}) {
|
||||
logger := pfxlog.Logger()
|
||||
|
||||
idx := strings.LastIndex(arg, ":")
|
||||
if idx == -1 {
|
||||
log.Error("Invalid argument")
|
||||
}
|
||||
|
||||
port := arg[idx+1:]
|
||||
serviceName := arg[0:idx]
|
||||
|
||||
logger = logger.WithField("service", serviceName)
|
||||
|
||||
if id, ok, err := context.GetServiceId(serviceName); ok {
|
||||
// pre-fetch network session
|
||||
if ns, err := context.GetNetworkSession(id); err != nil {
|
||||
logger.WithError(err).Error("failed to acquire network session")
|
||||
close <- err
|
||||
return
|
||||
} else {
|
||||
logger.WithField("id", ns.Id).Debug("acquired network session")
|
||||
}
|
||||
} else if err != nil {
|
||||
logger.Fatalln(err)
|
||||
close <- err
|
||||
return
|
||||
} else {
|
||||
logger.Error("service is not accessible")
|
||||
close <- fmt.Errorf("service is not accessible")
|
||||
return
|
||||
}
|
||||
|
||||
server, err := net.Listen("tcp4", "0.0.0.0:"+port)
|
||||
if err != nil {
|
||||
logger.Fatalln(err)
|
||||
close <- err
|
||||
return
|
||||
}
|
||||
|
||||
logger = logger.WithField("addr", server.Addr().String())
|
||||
|
||||
logger.Info("service is listening on")
|
||||
defer logger.Info("service stopped")
|
||||
defer func() {
|
||||
close <- fmt.Sprintf("service listener %s exited", serviceName)
|
||||
}()
|
||||
|
||||
for {
|
||||
conn, err := server.Accept()
|
||||
if err != nil {
|
||||
logger.WithError(err).Error("accept failed")
|
||||
close <- err
|
||||
return
|
||||
}
|
||||
go RunConnection(serviceName, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func RunConnection(service string, conn net.Conn) {
|
||||
logger := pfxlog.Logger().WithFields(logrus.Fields{
|
||||
"service": service,
|
||||
"client": conn.RemoteAddr().String()})
|
||||
|
||||
c := conn.RemoteAddr().String()
|
||||
logger.Debugln("accepted")
|
||||
|
||||
proxyConn, err := context.Dial(service)
|
||||
if err != nil {
|
||||
logger.WithError(err).Errorf("dial error")
|
||||
return
|
||||
}
|
||||
done1 := make(chan int64)
|
||||
done2 := make(chan int64)
|
||||
|
||||
go myCopy(c, service, conn, proxyConn, done1)
|
||||
go myCopy(c, service, proxyConn, conn, done2)
|
||||
|
||||
for count := 2; count > 0; {
|
||||
select {
|
||||
case n1 := <-done1:
|
||||
logger.WithField("bytes", n1).Debug("received")
|
||||
case n2 := <-done2:
|
||||
logger.WithField("bytes", n2).Debug("sent")
|
||||
}
|
||||
count = count - 1
|
||||
}
|
||||
logger.Debug("done")
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
func myCopy(clt string, service string, dst io.WriteCloser, src io.ReadCloser, done chan int64) {
|
||||
defer dst.Close()
|
||||
n, err := io.Copy(dst, src)
|
||||
if err != nil {
|
||||
log.WithFields(logrus.Fields{"client": clt, "service": service}).Error(err.Error())
|
||||
}
|
||||
done <- n
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package subcmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
root.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging")
|
||||
root.PersistentFlags().StringVar(&logFormatter, "log-formatter", "", "Specify log formatter [json|pfxlog|text]")
|
||||
}
|
||||
|
||||
var root = &cobra.Command{
|
||||
Use: "ziti-proxy",
|
||||
Short: "Ziti Proxy",
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
if verbose {
|
||||
logrus.SetLevel(logrus.DebugLevel)
|
||||
}
|
||||
|
||||
switch logFormatter {
|
||||
case "pfxlog":
|
||||
logrus.SetFormatter(pfxlog.NewFormatterStartingToday())
|
||||
case "json":
|
||||
logrus.SetFormatter(&logrus.JSONFormatter{})
|
||||
case "text":
|
||||
logrus.SetFormatter(&logrus.TextFormatter{})
|
||||
default:
|
||||
// let logrus do its own thing
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var verbose bool
|
||||
var logFormatter string
|
||||
|
||||
func Execute() {
|
||||
if err := root.Execute(); err != nil {
|
||||
fmt.Printf("error: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package subcmd
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/common/version"
|
||||
"fmt"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
root.AddCommand(versionCmd)
|
||||
}
|
||||
|
||||
var versionCmd = &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Show component version",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println(version.GetBuildMetadata(verbose))
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/identity/identity"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ZtAPI string `json:"ztAPI"`
|
||||
ID identity.IdentityConfig `json:"id"`
|
||||
}
|
||||
|
||||
func New(ztApi string, idConfig identity.IdentityConfig) *Config {
|
||||
return &Config{
|
||||
ZtAPI: ztApi,
|
||||
ID: idConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func NewFromFile(confFile string) (*Config, error) {
|
||||
conf, err := ioutil.ReadFile(confFile)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Config file (%s) is not found ", confFile))
|
||||
}
|
||||
|
||||
c := Config{}
|
||||
err = json.Unmarshal(conf, &c)
|
||||
if err != nil {
|
||||
return nil, errors.New(fmt.Sprintf("Failed to load ZT configuration (%s): %v", confFile, err))
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"net/url"
|
||||
"path"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
IdentityTypeGateway = "gateway"
|
||||
IdentityTypeEndpoint = "endpoint"
|
||||
IdentityTypeCaEndpoint = "ca-endpoint"
|
||||
|
||||
AudienceGatewayEnroller = "gateway-enroller"
|
||||
AudienceEndpointEnroller = "endpoint-enroller"
|
||||
)
|
||||
|
||||
type Versions struct {
|
||||
Api string `json:"api"`
|
||||
EnrollmentApi string `json:"enrollmentApi"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
Type string `json:"type"`
|
||||
Id string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type EnrollmentClaims struct {
|
||||
EnrollmentMethod string `json:"em"`
|
||||
SignatureCert *x509.Certificate `json:"-"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
func (t *EnrollmentClaims) EnrolmentUrl() string {
|
||||
enrollmentUrl, err := url.Parse(t.Issuer)
|
||||
|
||||
if err != nil {
|
||||
pfxlog.Logger().WithError(err).WithField("url", t.Issuer).Panic("could not parse issuer as URL")
|
||||
}
|
||||
|
||||
enrollmentUrl.Path = path.Join(enrollmentUrl.Path, "enroll")
|
||||
query := enrollmentUrl.Query()
|
||||
query.Add("method", t.EnrollmentMethod)
|
||||
query.Add("token", t.Id)
|
||||
enrollmentUrl.RawQuery = query.Encode()
|
||||
|
||||
return enrollmentUrl.String()
|
||||
}
|
||||
|
||||
func (t *EnrollmentClaims) ToMapClaims() (jwt.MapClaims, error) {
|
||||
mapClaims := map[string]interface{}{}
|
||||
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
DecodeHook: nil,
|
||||
ErrorUnused: false,
|
||||
ZeroFields: false,
|
||||
WeaklyTypedInput: false,
|
||||
Metadata: nil,
|
||||
Result: &mapClaims, //map pointer required
|
||||
TagName: "json",
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not create decoder: %s", err)
|
||||
}
|
||||
|
||||
if err = decoder.Decode(t); err != nil {
|
||||
return nil, fmt.Errorf("could not decode: %s", err)
|
||||
}
|
||||
|
||||
if std, found := mapClaims["StandardClaims"]; found {
|
||||
delete(mapClaims, "StandardClaims")
|
||||
|
||||
if stdMap, ok := std.(map[string]interface{}); ok {
|
||||
for k, v := range stdMap {
|
||||
if !isZeroValue(v) {
|
||||
mapClaims[k] = v
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("could not converted standard claims section to map")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return mapClaims, nil
|
||||
}
|
||||
|
||||
func isZeroValue(x interface{}) bool {
|
||||
return x == reflect.Zero(reflect.TypeOf(x)).Interface()
|
||||
}
|
||||
|
||||
func (t EnrollmentClaims) Valid() error {
|
||||
return t.StandardClaims.Valid()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Addr struct {
|
||||
MsgCh MsgChannel
|
||||
}
|
||||
|
||||
func (e *Addr) Network() string {
|
||||
return "ziti-edge"
|
||||
}
|
||||
|
||||
func (e *Addr) String() string {
|
||||
return fmt.Sprintf(":%v", e.MsgCh.Id())
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
"github.com/netfoundry/ziti-foundation/transport"
|
||||
"github.com/netfoundry/ziti-foundation/transport/tls"
|
||||
"github.com/netfoundry/ziti-foundation/util/sequence"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type addrParser struct {
|
||||
p tls.AddressParser
|
||||
}
|
||||
|
||||
func (ap addrParser) Parse(addressString string) (transport.Address, error) {
|
||||
return ap.p.Parse(strings.Replace(addressString, "/", "", -1))
|
||||
}
|
||||
|
||||
func init() {
|
||||
transport.AddAddressParser(new(addrParser))
|
||||
}
|
||||
|
||||
type ConnFactory interface {
|
||||
io.Closer
|
||||
IsClosed() bool
|
||||
NewConn(service string) Conn
|
||||
}
|
||||
|
||||
type Identifiable interface {
|
||||
Id() uint32
|
||||
}
|
||||
|
||||
type Conn interface {
|
||||
net.Conn
|
||||
Identifiable
|
||||
NewConn(service string) Conn
|
||||
Connect(session *NetworkSession) (net.Conn, error)
|
||||
Listen(session *NetworkSession, serviceName string) (net.Listener, error)
|
||||
IsClosed() bool
|
||||
}
|
||||
|
||||
type MsgChannel struct {
|
||||
channel2.Channel
|
||||
id uint32
|
||||
msgIdSeq *sequence.Sequence
|
||||
writeDeadline time.Time
|
||||
trace bool
|
||||
}
|
||||
|
||||
func NewEdgeMsgChannel(ch channel2.Channel, connId uint32) *MsgChannel {
|
||||
traceEnabled := strings.EqualFold("true", os.Getenv("ZITI_TRACE_ENABLED"))
|
||||
if traceEnabled {
|
||||
pfxlog.Logger().Info("Ziti message tracing ENABLED")
|
||||
}
|
||||
|
||||
return &MsgChannel{
|
||||
Channel: ch,
|
||||
id: connId,
|
||||
msgIdSeq: sequence.NewSequence(),
|
||||
trace: traceEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func (ec *MsgChannel) Id() uint32 {
|
||||
return ec.id
|
||||
}
|
||||
|
||||
func (ec *MsgChannel) SetWriteDeadline(t time.Time) error {
|
||||
ec.writeDeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ec *MsgChannel) Write(data []byte) (n int, err error) {
|
||||
return ec.WriteTraced(data, nil)
|
||||
}
|
||||
|
||||
func (ec *MsgChannel) WriteTraced(data []byte, msgUUID []byte) (int, error) {
|
||||
msg := NewDataMsg(ec.id, ec.msgIdSeq.Next(), data)
|
||||
if msgUUID != nil {
|
||||
msg.Headers[UUIDHeader] = msgUUID
|
||||
}
|
||||
ec.TraceMsg("write", msg)
|
||||
pfxlog.Logger().WithFields(GetLoggerFields(msg)).Debugf("writing %v bytes", len(data))
|
||||
|
||||
// NOTE: We need to wait for the buffer to be on the wire before returning. The Writer contract
|
||||
// states that buffers are not allowed be retained, and if we have it queued asynchronously
|
||||
// it is retained and we can cause data corruption
|
||||
var err error
|
||||
if ec.writeDeadline.IsZero() {
|
||||
var errC chan error
|
||||
errC, err = ec.Channel.SendAndSync(msg)
|
||||
if err == nil {
|
||||
err = <-errC
|
||||
}
|
||||
} else {
|
||||
err = ec.Channel.SendWithTimeout(msg, time.Until(ec.writeDeadline))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (ec *MsgChannel) SendState(msg *channel2.Message) error {
|
||||
msg.PutUint32Header(SeqHeader, ec.msgIdSeq.Next())
|
||||
ec.TraceMsg("SendState", msg)
|
||||
syncC, err := ec.SendAndSyncWithPriority(msg, channel2.High)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case err = <-syncC:
|
||||
return err
|
||||
case <-time.After(time.Second * 5):
|
||||
return errors.New("timed out waiting for close message send to complete")
|
||||
}
|
||||
}
|
||||
|
||||
func (ec *MsgChannel) TraceMsg(source string, msg *channel2.Message) {
|
||||
msgUUID, found := msg.Headers[UUIDHeader]
|
||||
if ec.trace && !found {
|
||||
newUUID, err := uuid.NewRandom()
|
||||
if err == nil {
|
||||
msgUUID = newUUID[:]
|
||||
msg.Headers[UUIDHeader] = msgUUID
|
||||
} else {
|
||||
pfxlog.Logger().WithField("connId", ec.id).WithError(err).Infof("failed to create trace uuid")
|
||||
}
|
||||
}
|
||||
|
||||
if msgUUID != nil {
|
||||
pfxlog.Logger().WithFields(GetLoggerFields(msg)).WithField("source", source).Debug("tracing message")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_edgeChannel_Host(t *testing.T) {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
"github.com/netfoundry/ziti-foundation/util/uuidz"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
ContentTypeConnect = 60783
|
||||
ContentTypeStateConnected = 60784
|
||||
ContentTypeStateClosed = 60785
|
||||
ContentTypeData = 60786
|
||||
ContentTypeDial = 60787
|
||||
ContentTypeDialSuccess = 60788
|
||||
ContentTypeDialFailed = 60789
|
||||
ContentTypeBind = 60790
|
||||
ContentTypeUnbind = 60791
|
||||
ContentTypeStateSessionEnded = 60792
|
||||
|
||||
ConnIdHeader = 1000
|
||||
SeqHeader = 1001
|
||||
SessionTokenHeader = 1002
|
||||
|
||||
// Put this in the reflected range so replies will share the same UUID
|
||||
UUIDHeader = 128
|
||||
)
|
||||
|
||||
var ContentTypeValue = map[string]int32{
|
||||
"EdgeConnectType": ContentTypeConnect,
|
||||
"EdgeStateConnectedType": ContentTypeStateConnected,
|
||||
"EdgeStateClosedType": ContentTypeStateClosed,
|
||||
"EdgeDataType": ContentTypeData,
|
||||
"EdgeDialType": ContentTypeDial,
|
||||
"EdgeDialSuccessType": ContentTypeDialSuccess,
|
||||
"EdgeDialFailedType": ContentTypeDialFailed,
|
||||
"EdgeBindType": ContentTypeBind,
|
||||
"EdgeUnbindType": ContentTypeUnbind,
|
||||
}
|
||||
|
||||
var ContentTypeNames = map[int32]string{
|
||||
ContentTypeConnect: "EdgeConnectType",
|
||||
ContentTypeStateConnected: "EdgeStateConnectedType",
|
||||
ContentTypeStateClosed: "EdgeStateClosedType",
|
||||
ContentTypeData: "EdgeDataType",
|
||||
ContentTypeDial: "EdgeDialType",
|
||||
ContentTypeDialSuccess: "EdgeDialSuccessType",
|
||||
ContentTypeDialFailed: "EdgeDialFailedType",
|
||||
ContentTypeBind: "EdgeBindType",
|
||||
ContentTypeUnbind: "EdgeUnbindType",
|
||||
}
|
||||
|
||||
type Sequenced interface {
|
||||
GetSequence() uint32
|
||||
}
|
||||
|
||||
type MsgEvent struct {
|
||||
ConnId uint32
|
||||
Seq uint32
|
||||
MsgUUID []byte
|
||||
Msg *channel2.Message
|
||||
}
|
||||
|
||||
func (event *MsgEvent) GetSequence() uint32 {
|
||||
return event.Seq
|
||||
}
|
||||
|
||||
func UnmarshalMsgEvent(msg *channel2.Message) (*MsgEvent, error) {
|
||||
connId, found := msg.GetUint32Header(ConnIdHeader)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("received edge message with no connId header. content type: %v", msg.ContentType)
|
||||
}
|
||||
seq, _ := msg.GetUint32Header(SeqHeader)
|
||||
|
||||
event := &MsgEvent{
|
||||
ConnId: connId,
|
||||
Seq: seq,
|
||||
MsgUUID: msg.Headers[UUIDHeader],
|
||||
Msg: msg,
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func (event *MsgEvent) GetLoggerFields() logrus.Fields {
|
||||
msgUUID := uuidz.ToString(event.MsgUUID)
|
||||
connId, _ := event.Msg.GetUint32Header(ConnIdHeader)
|
||||
seq, _ := event.Msg.GetUint32Header(SeqHeader)
|
||||
|
||||
fields := logrus.Fields{
|
||||
"connId": connId,
|
||||
"type": ContentTypeNames[event.Msg.ContentType],
|
||||
"chSeq": event.Msg.Sequence(),
|
||||
"edgeSeq": seq,
|
||||
}
|
||||
|
||||
if msgUUID != "" {
|
||||
fields["uuid"] = msgUUID
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
func SequencerF(v interface{}) uint {
|
||||
event := v.(Sequenced)
|
||||
return uint(event.GetSequence())
|
||||
}
|
||||
|
||||
func newMsg(contentType int32, connId uint32, seq uint32, data []byte) *channel2.Message {
|
||||
msg := channel2.NewMessage(contentType, data)
|
||||
msg.PutUint32Header(ConnIdHeader, connId)
|
||||
msg.PutUint32Header(SeqHeader, seq)
|
||||
return msg
|
||||
}
|
||||
|
||||
func NewDataMsg(connId uint32, seq uint32, data []byte) *channel2.Message {
|
||||
return newMsg(ContentTypeData, connId, seq, data)
|
||||
}
|
||||
|
||||
func NewConnectMsg(connId uint32, token string) *channel2.Message {
|
||||
return newMsg(ContentTypeConnect, connId, 0, []byte(token))
|
||||
}
|
||||
|
||||
func NewStateConnectedMsg(connId uint32) *channel2.Message {
|
||||
return newMsg(ContentTypeStateConnected, connId, 0, nil)
|
||||
}
|
||||
|
||||
func NewStateClosedMsg(connId uint32, message string) *channel2.Message {
|
||||
return newMsg(ContentTypeStateClosed, connId, 0, []byte(message))
|
||||
}
|
||||
|
||||
func NewDialMsg(connId uint32, token string) *channel2.Message {
|
||||
return newMsg(ContentTypeDial, connId, 0, []byte(token))
|
||||
}
|
||||
|
||||
func NewBindMsg(connId uint32, token string) *channel2.Message {
|
||||
return newMsg(ContentTypeBind, connId, 0, []byte(token))
|
||||
}
|
||||
|
||||
func NewUnbindMsg(connId uint32, token string) *channel2.Message {
|
||||
return newMsg(ContentTypeUnbind, connId, 0, []byte(token))
|
||||
}
|
||||
|
||||
func NewDialSuccessMsg(connId uint32, newConnId uint32) *channel2.Message {
|
||||
newConnIdBytes := make([]byte, 4)
|
||||
binary.LittleEndian.PutUint32(newConnIdBytes, newConnId)
|
||||
msg := newMsg(ContentTypeDialSuccess, connId, 0, newConnIdBytes)
|
||||
return msg
|
||||
}
|
||||
|
||||
func NewDialFailedMsg(connId uint32, message string) *channel2.Message {
|
||||
return newMsg(ContentTypeDialFailed, connId, 0, []byte(message))
|
||||
}
|
||||
|
||||
func NewStateSessionEndedMsg(reason string) *channel2.Message {
|
||||
return newMsg(ContentTypeStateSessionEnded, 0, 0, []byte(reason))
|
||||
}
|
||||
|
||||
type DialResult struct {
|
||||
ConnId uint32
|
||||
NewConnId uint32
|
||||
Success bool
|
||||
Message string
|
||||
}
|
||||
|
||||
func UnmarshalDialResult(msg *channel2.Message) (*DialResult, error) {
|
||||
connId, found := msg.GetUint32Header(ConnIdHeader)
|
||||
if !found {
|
||||
return nil, fmt.Errorf("received edge message with no connection id header")
|
||||
}
|
||||
|
||||
if msg.ContentType == ContentTypeDialSuccess {
|
||||
if len(msg.Body) != 4 {
|
||||
return nil, fmt.Errorf("dial success msg improperly formated. body len: %v", len(msg.Body))
|
||||
}
|
||||
newConnId := binary.LittleEndian.Uint32(msg.Body)
|
||||
return &DialResult{
|
||||
ConnId: connId,
|
||||
NewConnId: newConnId,
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if msg.ContentType == ContentTypeDialFailed {
|
||||
return &DialResult{
|
||||
ConnId: connId,
|
||||
Success: false,
|
||||
Message: string(msg.Body),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unexpected response. received %v instead of dial result message", msg.ContentType)
|
||||
}
|
||||
|
||||
func GetLoggerFields(msg *channel2.Message) logrus.Fields {
|
||||
msgUUID := uuidz.ToString(msg.Headers[UUIDHeader])
|
||||
|
||||
connId, _ := msg.GetUint32Header(ConnIdHeader)
|
||||
seq, _ := msg.GetUint32Header(SeqHeader)
|
||||
|
||||
fields := logrus.Fields{
|
||||
"connId": connId,
|
||||
"type": ContentTypeNames[msg.ContentType],
|
||||
"chSeq": msg.Sequence(),
|
||||
"edgeSeq": seq,
|
||||
}
|
||||
|
||||
if msgUUID != "" {
|
||||
fields["uuid"] = msgUUID
|
||||
}
|
||||
|
||||
return fields
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
"github.com/netfoundry/ziti-foundation/util/concurrenz"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MsgSink interface {
|
||||
io.Closer
|
||||
Id() uint32
|
||||
Accept(event *MsgEvent)
|
||||
}
|
||||
|
||||
func NewMsgMux() *MsgMux {
|
||||
mux := &MsgMux{
|
||||
eventC: make(chan MuxEvent),
|
||||
chanMap: make(map[uint32]MsgSink),
|
||||
}
|
||||
|
||||
mux.running.Set(true)
|
||||
go mux.handleEvents()
|
||||
return mux
|
||||
}
|
||||
|
||||
type MsgMux struct {
|
||||
closed concurrenz.AtomicBoolean
|
||||
running concurrenz.AtomicBoolean
|
||||
eventC chan MuxEvent
|
||||
chanMap map[uint32]MsgSink
|
||||
}
|
||||
|
||||
func (mux *MsgMux) ContentType() int32 {
|
||||
return ContentTypeData
|
||||
}
|
||||
|
||||
func (mux *MsgMux) HandleReceive(msg *channel2.Message, ch channel2.Channel) {
|
||||
if event, err := UnmarshalMsgEvent(msg); err != nil {
|
||||
pfxlog.Logger().WithError(err).Errorf("error unmarshaling edge message headers. content type: %v", msg.ContentType)
|
||||
} else {
|
||||
mux.eventC <- event
|
||||
}
|
||||
}
|
||||
|
||||
func (mux *MsgMux) AddMsgSink(sink MsgSink) error {
|
||||
if !mux.closed.Get() {
|
||||
event := &muxAddSinkEvent{sink: sink, doneC: make(chan error)}
|
||||
mux.eventC <- event
|
||||
err, ok := <-event.doneC // wait for event to be done processing
|
||||
if ok && err != nil {
|
||||
return err
|
||||
}
|
||||
pfxlog.Logger().WithField("connId", sink.Id()).Debug("added to msg mux")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mux *MsgMux) RemoveMsgSink(sink MsgSink) {
|
||||
mux.RemoveMsgSinkById(sink.Id())
|
||||
}
|
||||
|
||||
func (mux *MsgMux) RemoveMsgSinkById(sinkId uint32) (MsgSink, bool) {
|
||||
if !mux.closed.Get() {
|
||||
event := &muxRemoveSinkEvent{
|
||||
sinkId: sinkId,
|
||||
doneC: make(chan MsgSink),
|
||||
}
|
||||
mux.eventC <- event
|
||||
sink, ok := <-event.doneC
|
||||
if ok && sink != nil {
|
||||
pfxlog.Logger().WithField("connId", sinkId).Debug("removed from msg mux")
|
||||
return sink, true
|
||||
}
|
||||
}
|
||||
pfxlog.Logger().WithField("connId", sinkId).Debug("sink not found in msg mux")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (mux *MsgMux) Close() {
|
||||
if !mux.closed.Get() {
|
||||
mux.eventC <- &muxCloseEvent{}
|
||||
}
|
||||
}
|
||||
|
||||
func (mux *MsgMux) Event(event MuxEvent) {
|
||||
if !mux.closed.Get() {
|
||||
mux.eventC <- event
|
||||
}
|
||||
}
|
||||
|
||||
func (mux *MsgMux) IsClosed() bool {
|
||||
return mux.closed.Get()
|
||||
}
|
||||
|
||||
func (mux *MsgMux) HandleClose(ch channel2.Channel) {
|
||||
mux.Close()
|
||||
}
|
||||
|
||||
func (mux *MsgMux) handleEvents() {
|
||||
defer mux.running.Set(false)
|
||||
for event := range mux.eventC {
|
||||
event.Handle(mux)
|
||||
if mux.closed.GetUnsafe() {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (mux *MsgMux) ExecuteClose() {
|
||||
mux.closed.Set(true)
|
||||
for _, val := range mux.chanMap {
|
||||
if err := val.Close(); err != nil {
|
||||
pfxlog.Logger().
|
||||
WithField("sinkId", val.Id()).
|
||||
WithError(err).
|
||||
Error("error while closing message sink")
|
||||
}
|
||||
}
|
||||
|
||||
// make sure that anything trying to deliver events is freed
|
||||
for {
|
||||
select {
|
||||
case <-mux.eventC: // drop event
|
||||
case <-time.After(time.Millisecond * 100):
|
||||
close(mux.eventC)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MuxEvent interface {
|
||||
Handle(mux *MsgMux)
|
||||
}
|
||||
|
||||
// muxAddSinkEvent handles adding a new message sink to the mux
|
||||
type muxAddSinkEvent struct {
|
||||
sink MsgSink
|
||||
doneC chan error
|
||||
}
|
||||
|
||||
func (event *muxAddSinkEvent) Handle(mux *MsgMux) {
|
||||
defer close(event.doneC)
|
||||
if _, found := mux.chanMap[event.sink.Id()]; found {
|
||||
event.doneC <- fmt.Errorf("message sink with id %v already exists", event.sink.Id())
|
||||
} else {
|
||||
mux.chanMap[event.sink.Id()] = event.sink
|
||||
pfxlog.Logger().
|
||||
WithField("connId", event.sink.Id()).
|
||||
Debugf("Added sink to mux. Current sink count: %v", len(mux.chanMap))
|
||||
}
|
||||
}
|
||||
|
||||
// muxRemoveSinkEvent handles removing a closed message sink from the mux
|
||||
type muxRemoveSinkEvent struct {
|
||||
sinkId uint32
|
||||
doneC chan MsgSink
|
||||
}
|
||||
|
||||
func (event *muxRemoveSinkEvent) Handle(mux *MsgMux) {
|
||||
defer close(event.doneC)
|
||||
sink, found := mux.chanMap[event.sinkId]
|
||||
delete(mux.chanMap, event.sinkId)
|
||||
if found {
|
||||
event.doneC <- sink
|
||||
}
|
||||
pfxlog.Logger().
|
||||
WithField("connId", event.sinkId).
|
||||
Debugf("Removed sink from mux. Current sink count: %v", len(mux.chanMap))
|
||||
}
|
||||
|
||||
func (event *MsgEvent) Handle(mux *MsgMux) {
|
||||
logger := pfxlog.Logger().
|
||||
WithField("seq", event.Seq).
|
||||
WithField("connId", event.ConnId)
|
||||
|
||||
logger.Debugf("handling received %v", ContentTypeNames[event.Msg.ContentType])
|
||||
|
||||
if sink, found := mux.chanMap[event.ConnId]; found {
|
||||
sink.Accept(event)
|
||||
} else {
|
||||
logger.Debug("msg received for unknown edge channel id")
|
||||
}
|
||||
}
|
||||
|
||||
// muxCloseEvent handles closing the message multiplexer and all associated sinks
|
||||
type muxCloseEvent struct{}
|
||||
|
||||
func (event *muxCloseEvent) Handle(mux *MsgMux) {
|
||||
mux.ExecuteClose()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Test_newMsgMux(t *testing.T) {
|
||||
mux := NewMsgMux()
|
||||
assert := require.New(t)
|
||||
assert.True(mux.running.Get())
|
||||
assert.False(mux.closed.Get())
|
||||
mux.Close()
|
||||
assert.NoError(mux.closed.WaitForState(true, time.Millisecond * 100, time.Millisecond * 5))
|
||||
assert.NoError(mux.running.WaitForState(false, time.Millisecond * 150, time.Millisecond * 5))
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
)
|
||||
|
||||
type FunctionReceiveAdapter struct {
|
||||
Type int32
|
||||
Handler func(*channel2.Message, channel2.Channel)
|
||||
}
|
||||
|
||||
func (adapter *FunctionReceiveAdapter) ContentType() int32 {
|
||||
return adapter.Type
|
||||
}
|
||||
|
||||
func (adapter FunctionReceiveAdapter) HandleReceive(m *channel2.Message, ch channel2.Channel) {
|
||||
adapter.Handler(m, ch)
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
Id string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
//Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type Gateway struct {
|
||||
Name string `json:"name"`
|
||||
Hostname string `json:"hostname"`
|
||||
Urls map[string]string
|
||||
}
|
||||
|
||||
type NetworkSession struct {
|
||||
Id string `json:"id"`
|
||||
Token string `json:"token"`
|
||||
SessionId string `json:"sessionId"`
|
||||
Gateways []Gateway `json:"gateways"`
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
Name string `json:"name"`
|
||||
Id string `json:"id"`
|
||||
Dns struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Port int `json:"port"`
|
||||
} `json:"dns"`
|
||||
Hostable bool `json:"hostable"`
|
||||
Tags map[string]string `json:"tags"`
|
||||
}
|
||||
|
||||
type EdgeControllerApiError struct {
|
||||
Error struct {
|
||||
Args struct {
|
||||
URLVars struct {
|
||||
} `json:"urlVars"`
|
||||
} `json:"args"`
|
||||
Cause struct {
|
||||
Message string `json:"message"`
|
||||
FieldName string `json:"fieldName"`
|
||||
FieldValue string `json:"fieldValue"`
|
||||
} `json:"cause"`
|
||||
CauseMessage string `json:"causeMessage"`
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
RequestID string `json:"requestId"`
|
||||
} `json:"error"`
|
||||
Meta struct {
|
||||
APIEnrolmentVersion string `json:"apiEnrolmentVersion"`
|
||||
APIVersion string `json:"apiVersion"`
|
||||
} `json:"meta"`
|
||||
}
|
||||
|
||||
type apiResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Meta *ApiResponseMetadata `json:"meta"`
|
||||
}
|
||||
|
||||
type ApiResponseMetadata struct {
|
||||
FilterableFields []string `json:"filterableFields"`
|
||||
Pagination *struct {
|
||||
Offset int `json:"offset"`
|
||||
Limit int `json:"limit"`
|
||||
TotalCount int `json:"totalCount"`
|
||||
} `json:"pagination"`
|
||||
}
|
||||
|
||||
func ApiResponseDecode(data interface{}, resp io.Reader) (*ApiResponseMetadata, error) {
|
||||
apiR := &apiResponse{
|
||||
Data: data,
|
||||
}
|
||||
if err := json.NewDecoder(resp).Decode(apiR); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return apiR.Meta, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNetworkSessionDecode(t *testing.T) {
|
||||
resp := `
|
||||
{"meta":{},
|
||||
"data":{"_links":{"self":{"href":"./network-sessions/a7dde565-dec8-4188-90e5-42f5d33bf5a6"}},
|
||||
"gateways":[
|
||||
{"hostname":"hermes-host.ziti.netfoundry.io","name":"hermes","urls":{"tls":"tls://hermes-host.ziti.netfoundry.io:3022"}}],
|
||||
"id":"a7dde565-dec8-4188-90e5-42f5d33bf5a6","token":"75d9aa68-dde3-4243-a062-50fab347b781"}}
|
||||
`
|
||||
ns := new(NetworkSession)
|
||||
|
||||
_, err := ApiResponseDecode(ns, strings.NewReader(resp))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
gateways := make([]Gateway,1)
|
||||
gateways[0].Name = "hermes"
|
||||
gateways[0].Hostname = "hermes-host.ziti.netfoundry.io"
|
||||
gateways[0].Urls = map[string]string{
|
||||
"tls": "tls://hermes-host.ziti.netfoundry.io:3022",
|
||||
}
|
||||
expected := &NetworkSession{
|
||||
Token: "75d9aa68-dde3-4243-a062-50fab347b781",
|
||||
Id: "a7dde565-dec8-4188-90e5-42f5d33bf5a6",
|
||||
Gateways: gateways,
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(expected, ns) {
|
||||
t.Errorf("decode network session = %+v, want %+v", ns, expected)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package enroll
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/identity/certtools"
|
||||
"github.com/netfoundry/ziti-foundation/identity/identity"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/config"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
nfpem "github.com/netfoundry/ziti-foundation/util/pem"
|
||||
"github.com/netfoundry/ziti-foundation/util/x509"
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/fullsailor/pkcs7"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type EnrollmentFlags struct {
|
||||
Token *config.EnrollmentClaims
|
||||
JwtToken *jwt.Token
|
||||
JwtString string
|
||||
CertFile string
|
||||
KeyFile string
|
||||
IDName string
|
||||
AdditionalCAs string
|
||||
}
|
||||
|
||||
func ParseToken(tokenStr string) (*config.EnrollmentClaims, *jwt.Token, error) {
|
||||
parser := &jwt.Parser{
|
||||
SkipClaimsValidation: false,
|
||||
}
|
||||
enrollmentClaims := &config.EnrollmentClaims{}
|
||||
jwtToken, err := parser.ParseWithClaims(tokenStr, enrollmentClaims, ValidateToken)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return enrollmentClaims, jwtToken, nil
|
||||
}
|
||||
|
||||
func ValidateToken(token *jwt.Token) (interface{}, error) {
|
||||
if token == nil {
|
||||
return nil, fmt.Errorf("could not validate token, token is nil")
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*config.EnrollmentClaims)
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not validate token, token is not EnrollmentClaims")
|
||||
}
|
||||
|
||||
if claims == nil {
|
||||
return nil, fmt.Errorf("could not validate token, EnrollmentClaims are nil")
|
||||
}
|
||||
|
||||
if claims.Issuer == "" {
|
||||
return nil, fmt.Errorf("could not validate token, issues is empty")
|
||||
}
|
||||
|
||||
_, err := url.Parse(claims.Issuer)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not validate token, issuer [%s] is not a valid url ", claims.Issuer)
|
||||
}
|
||||
|
||||
cert, err := FetchServerCert(claims.Issuer)
|
||||
|
||||
claims.SignatureCert = cert
|
||||
|
||||
if err != nil || cert == nil {
|
||||
return nil, fmt.Errorf("could not retrieve token URL certificate: %s", err)
|
||||
}
|
||||
|
||||
return cert.PublicKey, nil
|
||||
}
|
||||
|
||||
func Enroll(enFlags EnrollmentFlags) (*config.Config, error) {
|
||||
var key crypto.PrivateKey
|
||||
var err error
|
||||
cfg := &config.Config{
|
||||
ZtAPI: enFlags.Token.Issuer,
|
||||
}
|
||||
|
||||
if strings.TrimSpace(enFlags.KeyFile) != "" {
|
||||
stat, err := os.Stat(enFlags.KeyFile)
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
if stat.IsDir() {
|
||||
return nil, fmt.Errorf("specified key is a directory (%s)", enFlags.KeyFile)
|
||||
}
|
||||
if absPath, fileErr := filepath.Abs(enFlags.KeyFile); fileErr != nil {
|
||||
return nil, fileErr
|
||||
} else {
|
||||
cfg.ID.Key = "file://" + absPath
|
||||
}
|
||||
} else {
|
||||
cfg.ID.Key = enFlags.KeyFile
|
||||
fmt.Printf("using engine : %s\n", strings.Split(enFlags.KeyFile, ":")[0])
|
||||
}
|
||||
} else {
|
||||
key, err = generateKey()
|
||||
asnBytes, _ := x509.MarshalECPrivateKey(key.(*ecdsa.PrivateKey))
|
||||
keyPem := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: asnBytes})
|
||||
cfg.ID.Key = "pem:" + string(keyPem)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
caPool := x509.NewCertPool()
|
||||
|
||||
allowedCerts := make([]*x509.Certificate, 0)
|
||||
|
||||
if strings.TrimSpace(enFlags.AdditionalCAs) != "" {
|
||||
pfxlog.Logger().Debug("adding certificates from the provided ca override file")
|
||||
pem, _ := ioutil.ReadFile(enFlags.AdditionalCAs)
|
||||
for _, xcert := range nfpem.PemToX509(string(pem)) {
|
||||
allowedCerts = append(allowedCerts, xcert)
|
||||
caPool.AddCert(xcert)
|
||||
}
|
||||
}
|
||||
|
||||
if enFlags.CertFile != "" {
|
||||
enFlags.CertFile, _ = filepath.Abs(enFlags.CertFile)
|
||||
cfg.ID.Cert = "file://" + enFlags.CertFile
|
||||
}
|
||||
|
||||
enrollmentComplete := false
|
||||
shouldFetchCerts := true
|
||||
|
||||
var enrollErr error
|
||||
|
||||
//loop two times at most. if the correct certs are in the jwt or the overridden ca file then the enrollment will function properly
|
||||
//if not - fetch the certificates from the server - add them to the caPool and try again a second time
|
||||
for !enrollmentComplete {
|
||||
switch enFlags.Token.EnrollmentMethod {
|
||||
case "ott":
|
||||
enrollErr = enrollOTT(enFlags.Token, cfg, caPool)
|
||||
case "ottca":
|
||||
enrollErr = enrollCA(enFlags.Token, cfg, caPool)
|
||||
case "ca":
|
||||
enrollErr = enrollCAAuto(enFlags, cfg, caPool)
|
||||
default:
|
||||
enrollErr = fmt.Errorf("enrollment method '%s' is not supported", enFlags.Token.EnrollmentMethod)
|
||||
enrollmentComplete = true //no need to try again
|
||||
}
|
||||
|
||||
if enrollErr == nil {
|
||||
enrollmentComplete = true //enrollment was successful
|
||||
} else {
|
||||
//determine if the failure is expected or due to tls. if tls related - retry. if not - just carry on without retrying
|
||||
urlErr, ok := enrollErr.(*url.Error)
|
||||
if ok {
|
||||
_, okx509 := urlErr.Err.(x509.UnknownAuthorityError)
|
||||
if okx509 && shouldFetchCerts {
|
||||
// don't try to fetch certs again
|
||||
shouldFetchCerts = false
|
||||
|
||||
pfxlog.Logger().Debug("fetching certificates from server")
|
||||
rootCaPool := x509.NewCertPool()
|
||||
rootCaPool.AddCert(enFlags.Token.SignatureCert)
|
||||
|
||||
for _, xcert := range FetchCertificates(cfg.ZtAPI, rootCaPool) {
|
||||
allowedCerts = append(allowedCerts, xcert)
|
||||
caPool.AddCert(xcert)
|
||||
}
|
||||
|
||||
//certs fetched - try again
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// if any error other than a tls-related error occurs just return it - don't try again
|
||||
return cfg, enrollErr
|
||||
}
|
||||
}
|
||||
|
||||
if len(allowedCerts) > 0 {
|
||||
var buf bytes.Buffer
|
||||
merr := nfx509.MarshalToPem(allowedCerts, &buf)
|
||||
if merr != nil {
|
||||
return nil, merr
|
||||
}
|
||||
cfg.ID.CA = "pem:" + buf.String()
|
||||
}
|
||||
|
||||
return cfg, nil // success
|
||||
}
|
||||
|
||||
func generateKey() (crypto.PrivateKey, error) {
|
||||
p384 := elliptic.P384()
|
||||
fmt.Printf("generating %s key\n", p384.Params().Name)
|
||||
return ecdsa.GenerateKey(p384, rand.Reader)
|
||||
}
|
||||
|
||||
func useSystemCasIfEmpty(caPool *x509.CertPool) *x509.CertPool {
|
||||
if len(caPool.Subjects()) < 1 {
|
||||
pfxlog.Logger().Debugf("no cas provided in caPool. using system provided cas")
|
||||
//this means that there were no ca's in the jwt and none fetched and added... fallback to using
|
||||
//the system defined ca pool in this case
|
||||
return nil
|
||||
} else {
|
||||
return caPool
|
||||
}
|
||||
}
|
||||
|
||||
func enrollOTT(token *config.EnrollmentClaims, cfg *config.Config, caPool *x509.CertPool) error {
|
||||
|
||||
pk, err := identity.LoadKey(cfg.ID.Key)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load private key '%s': %s", cfg.ID.Key, err.Error())
|
||||
}
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
request, err := certtools.NewCertRequest(map[string]string{
|
||||
"C": "US", "O": "Netfoundry", "CN": hostname,
|
||||
}, nil)
|
||||
|
||||
csr, err := x509.CreateCertificateRequest(rand.Reader, request, pk)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
csrPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr})
|
||||
|
||||
caPool = useSystemCasIfEmpty(caPool)
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: caPool,
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Post(token.EnrolmentUrl(), "text/plain", bytes.NewReader(csrPem))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK || err != nil {
|
||||
var edgeError edge.EdgeControllerApiError
|
||||
jsonerr := json.Unmarshal(respBody, &edgeError)
|
||||
|
||||
if jsonerr != nil {
|
||||
return fmt.Errorf("enroll error: %s. raw response: %s. ", resp.Status, respBody)
|
||||
}
|
||||
|
||||
//successfully parsed the json - make sure the code and cause message match and give a helpful message
|
||||
if edgeError.Error.Cause.Message == "identity not found" && edgeError.Error.Code == "INVALID_FIELD" {
|
||||
return fmt.Errorf("enroll error: %s. response: the provided token was invalid. either the token is not correct or it has already been used", resp.Status)
|
||||
} else {
|
||||
return fmt.Errorf("enroll error: %s. response: %s. ", resp.Status, edgeError.Error.Cause.Message)
|
||||
}
|
||||
}
|
||||
cfg.ID.Cert = "pem:" + string(respBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func enrollCA(token *config.EnrollmentClaims, cfg *config.Config, caPool *x509.CertPool) error {
|
||||
|
||||
if id, err := identity.LoadIdentity(cfg.ID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
clientCert := id.Cert()
|
||||
|
||||
caPool = useSystemCasIfEmpty(caPool)
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: caPool,
|
||||
Certificates: []tls.Certificate{*clientCert},
|
||||
},
|
||||
},
|
||||
}
|
||||
resp, err := client.Post(token.EnrolmentUrl(), "text/plain", bytes.NewReader([]byte{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusConflict {
|
||||
return fmt.Errorf("the provided identity has already been enrolled")
|
||||
} else {
|
||||
return fmt.Errorf("enroll error: %s", resp.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type autoEnrollInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func enrollCAAuto(enFlags EnrollmentFlags, cfg *config.Config, caPool *x509.CertPool) error {
|
||||
if id, err := identity.LoadIdentity(cfg.ID); err != nil {
|
||||
return err
|
||||
} else {
|
||||
clientCert := id.Cert()
|
||||
|
||||
caPool = useSystemCasIfEmpty(caPool)
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: caPool,
|
||||
Certificates: []tls.Certificate{*clientCert},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var postBody []byte
|
||||
|
||||
if strings.TrimSpace(enFlags.IDName) != "" {
|
||||
user := autoEnrollInput{
|
||||
Name: strings.TrimSpace(enFlags.IDName),
|
||||
}
|
||||
pb, merr := json.Marshal(user)
|
||||
if merr != nil {
|
||||
pfxlog.Logger().Warnf("problem converting name to json. Using the default name: %s", merr)
|
||||
}
|
||||
postBody = pb
|
||||
}
|
||||
|
||||
resp, postErr := client.Post(enFlags.Token.EnrolmentUrl(), "text/plain", bytes.NewReader(postBody))
|
||||
if postErr != nil {
|
||||
return postErr
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
if resp.StatusCode == http.StatusConflict {
|
||||
return fmt.Errorf("the provided identity has already been enrolled")
|
||||
} else {
|
||||
return fmt.Errorf("enroll error: %s", resp.Status)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func FetchServerCert(urlRoot string) (*x509.Certificate, error) {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
resp, err := client.Get(urlRoot)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not contact remote server [%s]: %s", urlRoot, err)
|
||||
}
|
||||
|
||||
if resp.TLS == nil || len(resp.TLS.PeerCertificates) == 0 {
|
||||
return nil, errors.New("peer certificate information is missing")
|
||||
}
|
||||
|
||||
return resp.TLS.PeerCertificates[0], nil
|
||||
}
|
||||
|
||||
// FetchCertificates will accecss the server insecurely to pull down the latest CA to be used to communicate with the
|
||||
// server adding certificates to the provided pool
|
||||
func FetchCertificates(urlRoot string, rootCaPool *x509.CertPool) []*x509.Certificate {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{RootCAs: rootCaPool},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
certStoreUrl, err := url.Parse(urlRoot)
|
||||
if err != nil {
|
||||
pfxlog.Logger().WithError(err).WithField("url", urlRoot).Panic("could not parse base url to retrieve CA store")
|
||||
}
|
||||
|
||||
certStoreUrl.Path = path.Join(certStoreUrl.Path, ".well-known/est/cacerts") //specified by rfc7030
|
||||
|
||||
resp, respErr := client.Get(certStoreUrl.String())
|
||||
|
||||
if respErr != nil {
|
||||
//if an error occurs, log the issue and just return a nil slice of certs
|
||||
pfxlog.Logger().Errorf("unable to retrieve certificates from server at %s. %s", urlRoot, respErr)
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
pfxlog.Logger().WithError(err).Error("could not close response body during certificate lookup")
|
||||
}
|
||||
}()
|
||||
|
||||
pkcs7b64, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
pfxlog.Logger().Warnf("could not read response. no certificates added from %s", urlRoot)
|
||||
return nil
|
||||
}
|
||||
|
||||
if resp.StatusCode >= http.StatusOK && resp.StatusCode < http.StatusMultipleChoices {
|
||||
|
||||
pkcs7Certs, _ := base64.StdEncoding.DecodeString(string(pkcs7b64))
|
||||
if pkcs7Certs != nil {
|
||||
certs, parseErr := pkcs7.Parse(pkcs7Certs)
|
||||
if parseErr != nil {
|
||||
pfxlog.Logger().Warnf("could not parse certificates. no certificates added from %s", urlRoot)
|
||||
return nil
|
||||
}
|
||||
return certs.Certificates
|
||||
}
|
||||
} else {
|
||||
pfxlog.Logger().Debugf("no certificates added from url. http response: %d, url: %s", resp.StatusCode, urlRoot)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge_impl
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-foundation/util/concurrenz"
|
||||
"github.com/netfoundry/ziti-foundation/util/sequence"
|
||||
"github.com/netfoundry/ziti-foundation/util/sequencer"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var connSeq *sequence.Sequence
|
||||
|
||||
func init() {
|
||||
connSeq = sequence.NewSequence()
|
||||
}
|
||||
|
||||
type edgeConn struct {
|
||||
edge.MsgChannel
|
||||
readQ sequencer.Sequencer
|
||||
leftover []byte
|
||||
msgMux *edge.MsgMux
|
||||
hosting sync.Map
|
||||
closed concurrenz.AtomicBoolean
|
||||
serviceId string
|
||||
readDeadline time.Time
|
||||
}
|
||||
|
||||
func (conn *edgeConn) Accept(event *edge.MsgEvent) {
|
||||
conn.TraceMsg("Accept", event.Msg)
|
||||
if event.Msg.ContentType == edge.ContentTypeDial {
|
||||
pfxlog.Logger().WithFields(edge.GetLoggerFields(event.Msg)).Info("received dial request")
|
||||
go conn.newChildConnection(event)
|
||||
} else if event.Msg.ContentType == edge.ContentTypeStateClosed && event.Seq == 0 {
|
||||
_ = conn.Close()
|
||||
} else if err := conn.readQ.PutSequenced(event.Seq, event); err != nil {
|
||||
pfxlog.Logger().WithFields(edge.GetLoggerFields(event.Msg)).WithError(err).
|
||||
Error("error pushing edge message to sequencer")
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *edgeConn) NewConn(service string) edge.Conn {
|
||||
id := connSeq.Next()
|
||||
|
||||
edgeCh := &edgeConn{
|
||||
MsgChannel: *edge.NewEdgeMsgChannel(conn.Channel, id),
|
||||
readQ: sequencer.NewSingleWriterSeq(DefaultMaxOutOfOrderMsgs),
|
||||
msgMux: conn.msgMux,
|
||||
serviceId: service,
|
||||
}
|
||||
|
||||
_ = conn.msgMux.AddMsgSink(edgeCh) // duplicate errors only happen on the server side, since client controls ids
|
||||
return edgeCh
|
||||
}
|
||||
|
||||
func (conn *edgeConn) IsClosed() bool {
|
||||
return conn.Channel.IsClosed()
|
||||
}
|
||||
|
||||
func (conn *edgeConn) Network() string {
|
||||
return "ziti"
|
||||
}
|
||||
|
||||
func (conn *edgeConn) String() string {
|
||||
return conn.serviceId
|
||||
}
|
||||
|
||||
func (conn *edgeConn) LocalAddr() net.Addr {
|
||||
return &edge.Addr{MsgCh: conn.MsgChannel}
|
||||
}
|
||||
|
||||
func (conn *edgeConn) RemoteAddr() net.Addr {
|
||||
return conn
|
||||
}
|
||||
|
||||
func (conn *edgeConn) SetDeadline(t time.Time) error {
|
||||
if err := conn.SetReadDeadline(t); err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.SetWriteDeadline(t)
|
||||
}
|
||||
|
||||
func (conn *edgeConn) SetReadDeadline(t time.Time) error {
|
||||
conn.readDeadline = t
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *edgeConn) HandleClose(ch channel2.Channel) {
|
||||
logger := pfxlog.Logger().WithField("connId", conn.Id())
|
||||
defer logger.Debug("received HandleClose from underlying channel, marking conn closed")
|
||||
conn.readQ.Close()
|
||||
conn.closed.Set(true)
|
||||
}
|
||||
|
||||
func (conn *edgeConn) Connect(session *edge.NetworkSession) (net.Conn, error) {
|
||||
logger := pfxlog.Logger().WithField("connId", conn.Id())
|
||||
|
||||
connectRequest := edge.NewConnectMsg(conn.Id(), session.Token)
|
||||
conn.TraceMsg("connect", connectRequest)
|
||||
replyMsg, err := conn.SendAndWaitWithTimeout(connectRequest, 5*time.Second)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if replyMsg.ContentType == edge.ContentTypeStateClosed {
|
||||
return nil, fmt.Errorf("attempt to use closed connection: %v", string(replyMsg.Body))
|
||||
}
|
||||
|
||||
if replyMsg.ContentType != edge.ContentTypeStateConnected {
|
||||
return nil, fmt.Errorf("unexpected response to connect attempt: %v", replyMsg.ContentType)
|
||||
}
|
||||
|
||||
logger.Debug("connected")
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (conn *edgeConn) Listen(session *edge.NetworkSession, serviceName string) (net.Listener, error) {
|
||||
logger := pfxlog.Logger().
|
||||
WithField("connId", conn.Id()).
|
||||
WithField("service", serviceName).
|
||||
WithField("session", session.Token)
|
||||
|
||||
logger.Debug("sending bind request to gateway")
|
||||
bindRequest := edge.NewBindMsg(conn.Id(), session.Token)
|
||||
conn.TraceMsg("listen", bindRequest)
|
||||
replyMsg, err := conn.SendAndWaitWithTimeout(bindRequest, 5*time.Second)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if replyMsg.ContentType == edge.ContentTypeStateClosed {
|
||||
msg := string(replyMsg.Body)
|
||||
logger.Debugf("bind request resulted in disconnect. msg: (%v)", msg)
|
||||
return nil, fmt.Errorf("attempt to use closed connection: %v", msg)
|
||||
}
|
||||
|
||||
if replyMsg.ContentType != edge.ContentTypeStateConnected {
|
||||
logger.Debugf("unexpected response to connect attempt: %v", replyMsg.ContentType)
|
||||
return nil, fmt.Errorf("unexpected response to connect attempt: %v", replyMsg.ContentType)
|
||||
}
|
||||
|
||||
logger.Debug("connected")
|
||||
listener := &edgeListener{
|
||||
serviceName: serviceName,
|
||||
token: session.Token,
|
||||
acceptC: make(chan net.Conn),
|
||||
edgeChan: conn,
|
||||
}
|
||||
conn.hosting.Store(session.Token, listener)
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
func (conn *edgeConn) Read(p []byte) (int, error) {
|
||||
log := pfxlog.Logger().WithField("connId", conn.Id())
|
||||
if conn.closed.Get() {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
log.Debugf("read buffer = %d bytes", cap(p))
|
||||
if len(conn.leftover) > 0 {
|
||||
log.Debugf("found %d leftover bytes", len(conn.leftover))
|
||||
n := copy(p, conn.leftover)
|
||||
conn.leftover = conn.leftover[n:]
|
||||
return n, nil
|
||||
}
|
||||
|
||||
for !conn.closed.Get() {
|
||||
next, err := conn.readQ.GetNextWithDeadline(conn.readDeadline)
|
||||
if err == sequencer.ErrClosed {
|
||||
log.Debug("sequencer closed, closing connection")
|
||||
conn.closed.Set(true)
|
||||
continue
|
||||
} else if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
event := next.(*edge.MsgEvent)
|
||||
switch event.Msg.ContentType {
|
||||
|
||||
case edge.ContentTypeStateClosed:
|
||||
conn.closed.Set(true)
|
||||
log.Debug("received ConnState_CLOSED message, closing connection")
|
||||
continue
|
||||
|
||||
case edge.ContentTypeData:
|
||||
d := event.Msg.Body
|
||||
log.Debugf("got buffer from queue %d bytes", len(d))
|
||||
if len(d) <= cap(p) {
|
||||
return copy(p, d), nil
|
||||
}
|
||||
conn.leftover = d[cap(p):]
|
||||
log.Debugf("saving %d bytes for leftover", len(conn.leftover))
|
||||
return copy(p, d), nil
|
||||
|
||||
default:
|
||||
log.WithField("type", event.Msg.ContentType).Error("unexpected message")
|
||||
}
|
||||
}
|
||||
log.Debug("return EOF from closing/closed connection")
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (conn *edgeConn) Close() error {
|
||||
if !conn.closed.CompareAndSwap(false, true) {
|
||||
return nil
|
||||
}
|
||||
|
||||
log := pfxlog.Logger().WithField("connId", conn.Id())
|
||||
log.Debug("close: begin")
|
||||
defer log.Debug("close: end")
|
||||
|
||||
msg := edge.NewStateClosedMsg(conn.Id(), "")
|
||||
if err := conn.SendState(msg); err != nil {
|
||||
log.WithError(err).Error("failed to send close message")
|
||||
}
|
||||
|
||||
conn.readQ.Close()
|
||||
conn.msgMux.RemoveMsgSink(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *edgeConn) getListener(token string) (*edgeListener, bool) {
|
||||
if val, found := conn.hosting.Load(token); found {
|
||||
listener, ok := val.(*edgeListener)
|
||||
return listener, ok
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (conn *edgeConn) newChildConnection(event *edge.MsgEvent) {
|
||||
message := event.Msg
|
||||
token := string(message.Body)
|
||||
logger := pfxlog.Logger().WithField("connId", conn.Id()).WithField("token", token)
|
||||
logger.Debug("looking up listener")
|
||||
listener, found := conn.getListener(token)
|
||||
if !found {
|
||||
logger.Warn("listener not found")
|
||||
reply := edge.NewDialFailedMsg(conn.Id(), "invalid token")
|
||||
reply.ReplyTo(message)
|
||||
if err := conn.SendWithTimeout(reply, time.Second*5); err != nil {
|
||||
logger.Errorf("Failed to send reply to dial request: (%v)", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug("listener found. generating id for new connection")
|
||||
id := connSeq.Next()
|
||||
|
||||
edgeCh := &edgeConn{
|
||||
MsgChannel: *edge.NewEdgeMsgChannel(conn.Channel, id),
|
||||
readQ: sequencer.NewSingleWriterSeq(DefaultMaxOutOfOrderMsgs),
|
||||
msgMux: conn.msgMux,
|
||||
}
|
||||
|
||||
_ = conn.msgMux.AddMsgSink(edgeCh) // duplicate errors only happen on the server side, since client controls ids
|
||||
|
||||
pfxlog.Logger().
|
||||
WithField("connId", id).
|
||||
WithField("parentConnId", conn.Id()).
|
||||
WithField("token", token).
|
||||
Debug("new connection established")
|
||||
|
||||
reply := edge.NewDialSuccessMsg(conn.Id(), edgeCh.Id())
|
||||
reply.ReplyTo(message)
|
||||
startMsg, err := conn.SendAndWaitWithTimeout(reply, time.Second*5)
|
||||
if err != nil {
|
||||
logger.Errorf("Failed to send reply to dial request: (%v)", err)
|
||||
return
|
||||
}
|
||||
|
||||
if startMsg.ContentType == edge.ContentTypeStateConnected {
|
||||
listener.acceptC <- edgeCh
|
||||
} else {
|
||||
logger.Errorf("failed to receive start after dial. got %v", startMsg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge_impl
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-foundation/util/sequencer"
|
||||
)
|
||||
|
||||
const (
|
||||
// TODO: Add configuration mechanism for the SDK
|
||||
DefaultMaxOutOfOrderMsgs = 5000
|
||||
)
|
||||
|
||||
type connFactory struct {
|
||||
ch channel2.Channel
|
||||
msgMux *edge.MsgMux
|
||||
}
|
||||
|
||||
func NewEdgeConnFactory(ch channel2.Channel) edge.ConnFactory {
|
||||
connFactory := &connFactory{
|
||||
ch: ch,
|
||||
msgMux: edge.NewMsgMux(),
|
||||
}
|
||||
|
||||
ch.AddReceiveHandler(&edge.FunctionReceiveAdapter{
|
||||
Type: edge.ContentTypeDial,
|
||||
Handler: connFactory.msgMux.HandleReceive,
|
||||
})
|
||||
|
||||
ch.AddReceiveHandler(&edge.FunctionReceiveAdapter{
|
||||
Type: edge.ContentTypeStateClosed,
|
||||
Handler: connFactory.msgMux.HandleReceive,
|
||||
})
|
||||
|
||||
// Since data is the common message type, it gets to be dispatched directly
|
||||
ch.AddReceiveHandler(connFactory.msgMux)
|
||||
ch.AddCloseHandler(connFactory.msgMux)
|
||||
|
||||
return connFactory
|
||||
}
|
||||
|
||||
func (conn *connFactory) NewConn(service string) edge.Conn {
|
||||
id := connSeq.Next()
|
||||
|
||||
edgeCh := &edgeConn{
|
||||
MsgChannel: *edge.NewEdgeMsgChannel(conn.ch, id),
|
||||
readQ: sequencer.NewSingleWriterSeq(DefaultMaxOutOfOrderMsgs),
|
||||
msgMux: conn.msgMux,
|
||||
serviceId: service,
|
||||
}
|
||||
|
||||
_ = conn.msgMux.AddMsgSink(edgeCh) // duplicate errors only happen on the server side, since client controls ids
|
||||
return edgeCh
|
||||
}
|
||||
|
||||
func (conn *connFactory) Close() error {
|
||||
return conn.ch.Close()
|
||||
}
|
||||
|
||||
func (conn *connFactory) IsClosed() bool {
|
||||
return conn.ch.IsClosed()
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package edge_impl
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"errors"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type edgeListener struct {
|
||||
serviceName string
|
||||
token string
|
||||
edgeChan *edgeConn
|
||||
acceptC chan net.Conn
|
||||
}
|
||||
|
||||
func (listener *edgeListener) Network() string {
|
||||
return "ziti"
|
||||
}
|
||||
|
||||
func (listener *edgeListener) String() string {
|
||||
return listener.serviceName
|
||||
}
|
||||
|
||||
func (listener *edgeListener) Accept() (net.Conn, error) {
|
||||
conn, ok := <-listener.acceptC
|
||||
if !ok {
|
||||
return nil, errors.New("listener is closed")
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (listener *edgeListener) Addr() net.Addr {
|
||||
return listener
|
||||
}
|
||||
|
||||
func (listener *edgeListener) Close() error {
|
||||
listener.edgeChan.hosting.Delete(listener.token)
|
||||
close(listener.acceptC)
|
||||
|
||||
edgeChan := listener.edgeChan
|
||||
defer edgeChan.Close()
|
||||
unbindRequest := edge.NewUnbindMsg(edgeChan.Id(), listener.token)
|
||||
if err := edgeChan.SendWithTimeout(unbindRequest, time.Second*5); err != nil {
|
||||
pfxlog.Logger().Errorf("unable to unbind session %v for connId %v", listener.token, edgeChan.Id())
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Printf(`USAGE: %s <service-name>
|
||||
connect to edge service\n`, os.Args[0])
|
||||
return
|
||||
}
|
||||
|
||||
context := ziti.NewContext()
|
||||
con, err := context.Dial(os.Args[1])
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = con.Write([]byte("Hello from GO-sdk!\n"))
|
||||
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := con.Read(buf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
break
|
||||
}
|
||||
|
||||
log.Println(string(buf[:n]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package ziti
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/channel2"
|
||||
"github.com/netfoundry/ziti-foundation/common/constants"
|
||||
"github.com/netfoundry/ziti-foundation/identity/identity"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/config"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/internal/edge_impl"
|
||||
"github.com/netfoundry/ziti-foundation/transport"
|
||||
"github.com/netfoundry/ziti-foundation/util/info"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
Authenticate() error
|
||||
Dial(serviceName string) (net.Conn, error)
|
||||
Listen(serviceName string) (net.Listener, error)
|
||||
GetServiceId(serviceName string) (string, bool, error)
|
||||
GetServices() ([]edge.Service, error)
|
||||
GetNetworkSession(id string) (*edge.NetworkSession, error)
|
||||
GetNetworkHostSession(id string) (*edge.NetworkSession, error)
|
||||
|
||||
// Close closes any connections open to gateways
|
||||
Close()
|
||||
}
|
||||
|
||||
var authUrl, _ = url.Parse("/authenticate?method=cert")
|
||||
var servicesUrl, _ = url.Parse("/services")
|
||||
var networkSessionUrl, _ = url.Parse("/network-sessions")
|
||||
|
||||
type contextImpl struct {
|
||||
config *config.Config
|
||||
initDone sync.Once
|
||||
session session
|
||||
gwConnFactories map[string]edge.ConnFactory
|
||||
connMutex sync.Mutex
|
||||
}
|
||||
|
||||
type session struct {
|
||||
id identity.Identity
|
||||
zitiUrl *url.URL
|
||||
tlsCtx *tls.Config
|
||||
clt http.Client
|
||||
session *edge.Session
|
||||
servicesMtx sync.RWMutex
|
||||
services []edge.Service
|
||||
netSessions sync.Map
|
||||
}
|
||||
|
||||
func NewContext() Context {
|
||||
return &contextImpl{gwConnFactories: make(map[string]edge.ConnFactory)}
|
||||
}
|
||||
|
||||
func NewContextWithConfig(config *config.Config) Context {
|
||||
return &contextImpl{gwConnFactories: make(map[string]edge.ConnFactory), config: config}
|
||||
}
|
||||
|
||||
func (context *contextImpl) ensureConfigPresent() error {
|
||||
if context.config != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
const configEnvVarName = "ZITI_SDK_CONFIG"
|
||||
// If configEnvVarName is set, try to use it.
|
||||
// The calling application may override this by calling NewContextWithConfig
|
||||
confFile := os.Getenv(configEnvVarName)
|
||||
|
||||
if confFile == "" {
|
||||
return fmt.Errorf("unable to configure ziti as config environment variable %v not populated", configEnvVarName)
|
||||
}
|
||||
|
||||
logrus.Infof("loading Ziti configuration from %s", confFile)
|
||||
cfg, err := config.NewFromFile(confFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading config file specified by ${%s}: %v", configEnvVarName, err)
|
||||
}
|
||||
context.config = cfg
|
||||
return nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) initialize() error {
|
||||
var err error
|
||||
context.initDone.Do(func() {
|
||||
err = context.initializer()
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (context *contextImpl) initializer() error {
|
||||
err := context.ensureConfigPresent()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
session := &context.session
|
||||
|
||||
session.zitiUrl, _ = url.Parse(context.config.ZtAPI)
|
||||
|
||||
id, err := identity.LoadIdentity(context.config.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.id = id
|
||||
session.tlsCtx = id.ClientTLSConfig()
|
||||
session.clt = http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: session.tlsCtx,
|
||||
},
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
if err = context.Authenticate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get services
|
||||
if session.services, err = context.getServices(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) Authenticate() error {
|
||||
logrus.Info("attempting to authenticate")
|
||||
context.session.netSessions = sync.Map{}
|
||||
session := &context.session
|
||||
|
||||
req := new(bytes.Buffer)
|
||||
json.NewEncoder(req).Encode(info.GetSdkInfo())
|
||||
resp, err := session.clt.Post(session.zitiUrl.ResolveReference(authUrl).String(), "application/json", req)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
logrus.Fatal("failed to authenticate with ZT controller")
|
||||
}
|
||||
|
||||
apiSessionResp := edge.Session{}
|
||||
|
||||
_, err = edge.ApiResponseDecode(&apiSessionResp, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logrus.
|
||||
WithField("token", apiSessionResp.Token).
|
||||
WithField("id", apiSessionResp.Id).
|
||||
Debugf("Got session: %v", apiSessionResp)
|
||||
session.session = &apiSessionResp
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) Dial(serviceName string) (net.Conn, error) {
|
||||
if err := context.initialize(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initilize context: (%v)", err)
|
||||
}
|
||||
id, ok := context.getServiceId(serviceName)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("service '%s' not found in ZT", serviceName)
|
||||
}
|
||||
|
||||
var conn net.Conn
|
||||
var err error
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
ns, err := context.GetNetworkSession(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
conn, err = context.dialSession(serviceName, ns)
|
||||
if err != nil && attempt == 0 {
|
||||
if strings.Contains(err.Error(), "closed") {
|
||||
context.deleteNetworkSession(id)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return conn, err
|
||||
}
|
||||
return nil, fmt.Errorf("unable to dial service '%s' (%v)", serviceName, err)
|
||||
}
|
||||
|
||||
func (context *contextImpl) dialSession(service string, netSession *edge.NetworkSession) (net.Conn, error) {
|
||||
edgeConnFactory, err := context.getGatewayConnFactory(netSession)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edgeConn := edgeConnFactory.NewConn(service)
|
||||
return edgeConn.Connect(netSession)
|
||||
}
|
||||
|
||||
func (context *contextImpl) Listen(serviceName string) (net.Listener, error) {
|
||||
if err := context.initialize(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initilize context: (%v)", err)
|
||||
}
|
||||
|
||||
if id, ok, _ := context.GetServiceId(serviceName); ok {
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
ns, err := context.GetNetworkHostSession(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listener, err := context.listenSession(ns, serviceName)
|
||||
if err != nil && attempt == 0 {
|
||||
if strings.Contains(err.Error(), "closed") {
|
||||
context.deleteNetworkSession(id)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return listener, err
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("service '%s' not found in ZT", serviceName)
|
||||
}
|
||||
|
||||
func (context *contextImpl) listenSession(netSession *edge.NetworkSession, serviceName string) (net.Listener, error) {
|
||||
edgeConnFactory, err := context.getGatewayConnFactory(netSession)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
edgeConn := edgeConnFactory.NewConn(serviceName)
|
||||
return edgeConn.Listen(netSession, serviceName)
|
||||
}
|
||||
|
||||
func (context *contextImpl) getGatewayConnFactory(netSession *edge.NetworkSession) (edge.ConnFactory, error) {
|
||||
logger := pfxlog.Logger().WithField("ns", netSession.Token)
|
||||
|
||||
if len(netSession.Gateways) == 0 {
|
||||
return nil, errors.New("no edge routers available")
|
||||
}
|
||||
gateway := netSession.Gateways[0]
|
||||
ingressUrl := gateway.Urls["tls"]
|
||||
|
||||
context.connMutex.Lock()
|
||||
defer context.connMutex.Unlock()
|
||||
|
||||
// remove any closed connections
|
||||
for key, val := range context.gwConnFactories {
|
||||
if val.IsClosed() {
|
||||
delete(context.gwConnFactories, key)
|
||||
}
|
||||
}
|
||||
|
||||
if edgeConn, found := context.gwConnFactories[ingressUrl]; found {
|
||||
return edgeConn, nil
|
||||
}
|
||||
|
||||
ingAddr, err := transport.ParseAddress(ingressUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := context.session.id
|
||||
dialer := channel2.NewClassicDialer(identity.NewIdentity(id), ingAddr, map[int32][]byte{
|
||||
edge.SessionTokenHeader: []byte(context.session.session.Token),
|
||||
})
|
||||
|
||||
ch, err := channel2.NewChannel("ziti-sdk", dialer, nil)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
edgeConn := edge_impl.NewEdgeConnFactory(ch)
|
||||
context.gwConnFactories[ingressUrl] = edgeConn
|
||||
return edgeConn, nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) GetServiceId(name string) (string, bool, error) {
|
||||
if err := context.initialize(); err != nil {
|
||||
return "", false, fmt.Errorf("failed to initilize context: (%v)", err)
|
||||
}
|
||||
|
||||
id, found := context.getServiceId(name)
|
||||
return id, found, nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) getServiceId(name string) (string, bool) {
|
||||
context.session.servicesMtx.RLock()
|
||||
defer context.session.servicesMtx.RUnlock()
|
||||
|
||||
for _, s := range context.session.services {
|
||||
if s.Name == name {
|
||||
return s.Id, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (context *contextImpl) GetServices() ([]edge.Service, error) {
|
||||
if err := context.initialize(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initilize context: (%v)", err)
|
||||
}
|
||||
return context.getServices()
|
||||
}
|
||||
|
||||
func (context *contextImpl) getServices() ([]edge.Service, error) {
|
||||
|
||||
servReq, _ := http.NewRequest("GET", context.session.zitiUrl.ResolveReference(servicesUrl).String(), nil)
|
||||
|
||||
if context.session.session.Token == "" {
|
||||
return nil, errors.New("session token is empty")
|
||||
}
|
||||
servReq.Header.Set(constants.ZitiSession, context.session.session.Token)
|
||||
pgOffset := 0
|
||||
pgLimit := 100
|
||||
servicesMap := make(map[string]edge.Service)
|
||||
|
||||
for {
|
||||
q := servReq.URL.Query()
|
||||
q.Set("limit", strconv.Itoa(pgLimit))
|
||||
q.Set("offset", strconv.Itoa(pgOffset))
|
||||
servReq.URL.RawQuery = q.Encode()
|
||||
resp, err := context.session.clt.Do(servReq)
|
||||
|
||||
if resp != nil && resp.StatusCode == http.StatusUnauthorized {
|
||||
return nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &[]edge.Service{}
|
||||
meta, err := edge.ApiResponseDecode(s, resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if meta == nil {
|
||||
// shouldn't happen
|
||||
return nil, errors.New("nil metadata in response to GET /services")
|
||||
}
|
||||
if meta.Pagination == nil {
|
||||
return nil, errors.New("nil pagination in response to GET /services")
|
||||
}
|
||||
|
||||
for _, svc := range *s {
|
||||
servicesMap[svc.Name] = svc
|
||||
}
|
||||
|
||||
pgOffset += pgLimit
|
||||
if pgOffset >= meta.Pagination.TotalCount {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
services := make([]edge.Service, len(servicesMap))
|
||||
i := 0
|
||||
for _, s := range servicesMap {
|
||||
services[i] = s
|
||||
i++
|
||||
}
|
||||
context.session.servicesMtx.Lock()
|
||||
context.session.services = services
|
||||
context.session.servicesMtx.Unlock()
|
||||
return services, nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) GetNetworkSession(id string) (*edge.NetworkSession, error) {
|
||||
return context.getNetworkSession(id, false)
|
||||
}
|
||||
|
||||
func (context *contextImpl) GetNetworkHostSession(id string) (*edge.NetworkSession, error) {
|
||||
return context.getNetworkSession(id, true)
|
||||
}
|
||||
|
||||
func (context *contextImpl) getNetworkSession(id string, host bool) (*edge.NetworkSession, error) {
|
||||
if err := context.initialize(); err != nil {
|
||||
return nil, fmt.Errorf("failed to initilize context: (%v)", err)
|
||||
}
|
||||
val, ok := context.session.netSessions.Load(id)
|
||||
if ok {
|
||||
return val.(*edge.NetworkSession), nil
|
||||
}
|
||||
body := fmt.Sprintf(`{"serviceId":"%s", "hosting": %s}`, id, strconv.FormatBool(host))
|
||||
reqBody := bytes.NewBufferString(body)
|
||||
|
||||
req, _ := http.NewRequest("POST", context.session.zitiUrl.ResolveReference(networkSessionUrl).String(), reqBody)
|
||||
req.Header.Set(constants.ZitiSession, context.session.session.Token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
logrus.WithField("service_id", id).Debug("requesting network session")
|
||||
resp, err := context.session.clt.Do(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 201 {
|
||||
respBody, _ := ioutil.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("Failed to create network session: %s\n%s", resp.Status, string(respBody))
|
||||
}
|
||||
|
||||
netSession := new(edge.NetworkSession)
|
||||
_, err = edge.ApiResponseDecode(netSession, resp.Body)
|
||||
if err != nil {
|
||||
pfxlog.Logger().Error("failed to decode net session response", err)
|
||||
return nil, err
|
||||
}
|
||||
context.session.netSessions.Store(id, netSession)
|
||||
|
||||
return netSession, nil
|
||||
}
|
||||
|
||||
func (context *contextImpl) deleteNetworkSession(id string) {
|
||||
context.session.netSessions.Delete(id)
|
||||
}
|
||||
|
||||
func (context *contextImpl) Close() {
|
||||
logger := pfxlog.Logger()
|
||||
|
||||
context.connMutex.Lock()
|
||||
defer context.connMutex.Unlock()
|
||||
|
||||
// remove any closed connections
|
||||
for key, val := range context.gwConnFactories {
|
||||
if !val.IsClosed() {
|
||||
if err := val.Close(); err != nil {
|
||||
logger.WithError(err).Error("error while closing connection")
|
||||
}
|
||||
}
|
||||
delete(context.gwConnFactories, key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
# ziti-tunnel
|
||||
|
||||
`ziti-tunnel` intercepts outbound transport layer packets to Ziti service addresses
|
||||
and proxies them through Ziti.
|
||||
|
||||
The services that are proxied are determined by the app-wan membership of the
|
||||
identity that is defined in the configuration file.
|
||||
|
||||
## Configuration File
|
||||
|
||||
A configuration file is created when an identity is successfully enrolled with
|
||||
a Ziti edge controller. Here's an example of a configuration file that
|
||||
`ziti-tunnel` accepts:
|
||||
|
||||
{
|
||||
"ztAPI": "https://ziti-dev-controller01.localhost:1080/",
|
||||
"versions": {
|
||||
"api": "1.0.0",
|
||||
"enrollmentApi": "1.0.0"
|
||||
},
|
||||
"id": {
|
||||
"cert": "file:///root/device01.3rdparty.client.cert.pem",
|
||||
"key": "file:///root/device01.3rdparty.client.key.pem",
|
||||
"ca": "file:///root/ziti-dev-ingress01.external.chain.cert.pem"
|
||||
}
|
||||
}
|
||||
|
||||
## Usage
|
||||
|
||||
ziti-tunnel authenticates with the edge controller in the config file, and
|
||||
periodically queries the controller for services that the identity is allowed to
|
||||
use. Whenever a new service is found, ziti-tunnel invokes the selected intercept
|
||||
mode with the details of the service.
|
||||
|
||||
`ziti-tunnel` supports several intercept modes, which are specified with a sub-command.
|
||||
Typically you will run `ziti-tunnel` with the _run_ sub-command, which determines
|
||||
the preferred intercept mode based on the kernel drivers that are available on the
|
||||
host.
|
||||
|
||||
$ sudo ziti-tunnel run ziti.json
|
||||
[ 0.000] INFO ziti/tunnel/intercept/tproxy.New: tproxy listening on 127.0.0.1:33641
|
||||
[ 0.006] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.run: using tproxy interceptor
|
||||
...
|
||||
|
||||
When started on a host that lacks __iptables__, the tproxy intercept mode initializer
|
||||
fails and `ziti-tunnel` attempts to use the tun intercept mode:
|
||||
|
||||
$ sudo ziti-tunnel run ziti.json
|
||||
[ 0.001] INFO ziti/tunnel/intercept/tproxy.New: tproxy listening on 127.0.0.1:37313
|
||||
[ 0.001] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.run: tproxy initialization failed: failed to initialize iptables handle: exec: "iptables": executable file not found in $PATH
|
||||
[ 0.009] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.run: using tun interceptor
|
||||
|
||||
`ziti-tunnel` fails to start if no intercept modes can be successfully initialized:
|
||||
|
||||
$ sudo ziti-tunnel run ziti.json
|
||||
[ 0.000] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.run: tproxy initialization failed: failed to initialize iptables handle: exec: "iptables": executable file not found in $PATH
|
||||
[ 0.001] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.run: tun initialization failed: failed to open tun interface (name='', mtu=0): ioctl failed with 'invalid argument'
|
||||
[ 0.001] FATAL ziti/tunnel/cmd/ziti-tunnel/subcmd.run: failed to initialize an interceptor
|
||||
|
||||
|
||||
## Intercept Modes
|
||||
|
||||
### tproxy
|
||||
|
||||
tproxy is the preferred intercept mode when running on a Linux kernel that has the
|
||||
__ip_tables__ kernel module installed.
|
||||
|
||||
$ lsmod | grep ip_tables
|
||||
ip_tables 32768 5 iptable_filter,iptable_security,iptable_raw,iptable_nat,iptable_mangle
|
||||
|
||||
`ziti-tunnel` manipulates routing tables and firewall rules when using the tproxy
|
||||
intercept mode. The NET_ADMIN Linux capability is required for these actions. The
|
||||
usage example here runs ziti-tunnel with sudo as a simple way to obtain
|
||||
NET_ADMIN:
|
||||
|
||||
$ sudo ziti-tunnel --identity ziti.json tproxy
|
||||
[ 0.000] INFO ziti/tunnel/intercept/tproxy.New: tproxy listening on 127.0.0.1:33355
|
||||
[ 0.010] INFO ziti/tunnel/dns.NewDnsServer: starting dns server...
|
||||
[ 2.018] INFO ziti/tunnel/dns.NewDnsServer: dns server running at 127.0.0.1:53
|
||||
[ 2.018] INFO ziti/tunnel/dns.(*resolver).AddHostname: adding ziti-tunnel.resolver.test = 19.65.28.94 to resolver
|
||||
[ 2.033] INFO ziti/tunnel/dns.(*resolver).RemoveHostname: removing ziti-tunnel.resolver.test from resolver
|
||||
[ 2.096] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: starting tunnel for newly available service wttr.in
|
||||
[ 2.290] INFO ziti/tunnel/dns.(*resolver).AddHostname: adding wttr.in = 5.9.243.187 to resolver
|
||||
[ 2.300] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: service wttr.in not hostable
|
||||
[ 2.300] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: starting tunnel for newly available service ssh-scarey
|
||||
[ 2.570] INFO ziti/tunnel/dns.(*resolver).AddHostname: adding scarey.io = 169.254.1.1 to resolver
|
||||
|
||||
The tproxy intercept mode creates a network listener that accepts connections at a
|
||||
randomly selected port on the loopback interface. Intercepted ziti service traffic
|
||||
directed to the listener by two mechanisms:
|
||||
|
||||
* Firewall Rules (iptables)
|
||||
|
||||
The TPROXY iptables target is the primary intercept mechanism used by the tproxy
|
||||
intercept mode. The TPROXY target essentially sends packets to a local listener
|
||||
without actually modifying the packet's destination address fields. See
|
||||
https://www.kernel.org/doc/Documentation/networking/tproxy.txt and
|
||||
`iptables-extensions(8)` for more details on the TPROXY target.
|
||||
|
||||
First, the tproxy interceptor links a new iptables chain to the PREROUTING chain:
|
||||
|
||||
$ sudo iptables -nt mangle -L PREROUTING | grep NF-INTERCEPT
|
||||
NF-INTERCEPT all -- 0.0.0.0/0 0.0.0.0/0
|
||||
|
||||
Then it creates rules in the new chain for each intercepted service. You can view
|
||||
the tproxy rules in play:
|
||||
|
||||
$ sudo iptables -nt mangle -L NF-INTERCEPT
|
||||
Chain NF-INTERCEPT (1 references)
|
||||
target prot opt source destination
|
||||
TPROXY tcp -- 0.0.0.0/0 5.9.243.187 /* wttr.in */ tcp dpt:443 TPROXY redirect 127.0.0.1:33355 mark 0x1/0x1
|
||||
TPROXY tcp -- 0.0.0.0/0 169.254.1.1 /* ssh-scarey */ tcp dpt:22 TPROXY redirect 127.0.0.1:33355 mark 0x1/0x1
|
||||
TPROXY tcp -- 0.0.0.0/0 1.2.3.4 /* netcat */ tcp dpt:22169 TPROXY redirect 127.0.0.1:33355 mark 0x1/0x1
|
||||
|
||||
Packets with a destination address that matches the intercept address of a Ziti
|
||||
service are directed to ziti-tunnel's network listener (127.0.0.1:33355 in the
|
||||
examples above). This effectively enables `ziti-tunnel` to capture packets that
|
||||
are destined for any address using a single listener (and a single port).
|
||||
|
||||
NOTE: _netfilter_ rules were considered when implementing ziti-tunnel's tproxy
|
||||
intercept mode. _netfilter_ is a slightly more modern than _iptables_ and has
|
||||
a supported netlink API for manipulating rules without "shelling out" to the
|
||||
`iptables` command line utility. _netfilter_ was ultimately abandoned because
|
||||
netfilter tproxy support requires kernel configuration options (`CONFIG_NFT_TPROXY`,
|
||||
`CONFIG_NFT_SOCKET`) that are not enabled in the default kernels of many common
|
||||
Linux distributions.
|
||||
|
||||
* Local Routes
|
||||
|
||||
The TPROXY target is only valid in the PREROUTING iptables chain, which is
|
||||
traversed by incoming packets that were routed to the host over the network. A
|
||||
_local_ route is necessary in order to get locally generated packets to traverse
|
||||
the PREROUTING chain:
|
||||
|
||||
$ ip route show table local
|
||||
local 1.2.3.4 dev lo proto kernel scope host src 1.2.3.4
|
||||
local 5.9.243.187 dev lo proto kernel scope host src 5.9.243.187
|
||||
local 169.254.1.1 dev lo proto kernel scope host src 169.254.1.1
|
||||
|
||||
### tun
|
||||
|
||||
The tun intercept mode creates an ephemeral tun interface and configures it with the
|
||||
IP addresses of the services that are being proxied.
|
||||
|
||||
`ziti-tunnel` manipulates network interfaces when using the tun intercept mode,
|
||||
which requires the NET_ADMIN Linux capability. The usage example here runs
|
||||
ziti-tunnel with sudo as a simple way to obtain NET_ADMIN:
|
||||
|
||||
$ sudo ziti-tunnel --identity ziti.json tun
|
||||
[ 0.010] INFO ziti/tunnel/dns.NewDnsServer: starting dns server...
|
||||
[ 2.012] INFO ziti/tunnel/dns.NewDnsServer: dns server running at 127.0.0.1:53
|
||||
[ 2.012] INFO ziti/tunnel/dns.(*resolver).AddHostname: adding ziti-tunnel.resolver.test = 19.65.28.94 to resolver
|
||||
[ 2.031] INFO ziti/tunnel/dns.(*resolver).RemoveHostname: removing ziti-tunnel.resolver.test from resolver
|
||||
[ 2.089] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: starting tunnel for newly available service wttr.in
|
||||
[ 2.280] INFO ziti/tunnel/dns.(*resolver).AddHostname: adding wttr.in = 5.9.243.187 to resolver
|
||||
[ 2.282] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: service wttr.in not hostable
|
||||
[ 2.282] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: starting tunnel for newly available service ssh-scarey
|
||||
[ 2.502] INFO ziti/tunnel/dns.(*resolver).AddHostname: adding scarey.io = 169.254.1.2 to resolver
|
||||
[ 2.505] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: service ssh-scarey not hostable
|
||||
[ 2.505] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: starting tunnel for newly available service netcat
|
||||
[ 2.506] INFO ziti/tunnel/cmd/ziti-tunnel/subcmd.updateServices: service netcat not hostable
|
||||
...
|
||||
|
||||
The addresses that `ziti-tunnel` adds to the tun interface are __point-to-point__
|
||||
addresses:
|
||||
|
||||
$ ip addr show dev tun0
|
||||
10: tun0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 65535 qdisc fq_codel state UNKNOWN group default qlen 500
|
||||
link/none
|
||||
inet 169.254.1.1/32 scope host tun0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet 169.254.1.1 peer 5.9.243.187/32 scope host tun0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet 169.254.1.1 peer 169.254.1.2/32 scope host tun0
|
||||
valid_lft forever preferred_lft forever
|
||||
inet 169.254.1.1 peer 1.2.3.4/32 scope host tun0
|
||||
valid_lft forever preferred_lft forever
|
||||
|
||||
The tun interface itself is assigned a link-local address, 169.254.1.1 in this
|
||||
case, and each intercepted service is represented by a point-to-point address with
|
||||
the remote address matching the intercept IP of the Ziti service. The tun intercept
|
||||
mode uses point-to-point addresses instead of local routes because local routes
|
||||
would result in the Linux networking stack receiving the packets that are routed to
|
||||
the tun interface. The point-to-point addresses ensure that the packets are delivered
|
||||
"to the wire", which mean that, for a tun interface, the packets will be picked up
|
||||
by `ziti-tunnel` when it reads data from the tun interface.
|
||||
|
||||
### proxy
|
||||
|
||||
The proxy intercept mode creates a network listener for each Ziti service that is
|
||||
intercepted. The services to intercept, and the ports that they are intercepted on,
|
||||
are specified on the command line (as opposed to using the service definitions that
|
||||
are retrieved from the edge controller):
|
||||
|
||||
$ ziti-tunnel --identity ziti.json proxy wttr.in:8443 ssh-scarey:2222 netcat:22169
|
||||
[ 0.004] INFO ziti/tunnel/intercept/proxy.(*proxyInterceptor).Start: starting proxy interceptor
|
||||
[ 0.120] INFO ziti/tunnel/intercept.updateServices: starting tunnel for newly available service ssh-scarey
|
||||
[ 0.183] INFO ziti/tunnel/intercept.updateServices: service ssh-scarey not hostable
|
||||
[ 0.183] INFO ziti/tunnel/intercept.updateServices: starting tunnel for newly available service netcat
|
||||
[ 0.183] INFO ziti/tunnel/intercept/proxy.proxyInterceptor.runServiceListener: {addr=[0.0.0.0:2222] service=[ssh-scarey]} service is listening
|
||||
[ 0.203] INFO ziti/tunnel/intercept.updateServices: service netcat not hostable
|
||||
[ 0.203] INFO ziti/tunnel/intercept/proxy.proxyInterceptor.runServiceListener: {addr=[0.0.0.0:22169] service=[netcat]} service is listening
|
||||
[ 0.203] INFO ziti/tunnel/intercept.updateServices: starting tunnel for newly available service wttr.in
|
||||
[ 0.226] INFO ziti/tunnel/intercept.updateServices: service wttr.in not hostable
|
||||
[ 0.226] INFO ziti/tunnel/intercept/proxy.proxyInterceptor.runServiceListener: {addr=[0.0.0.0:8443] service=[wttr.in]} service is listening
|
||||
|
||||
All network listeners bind to local network interfaces (0.0.0.0):
|
||||
|
||||
$ netstat -tnl | fgrep 0.0.0.0
|
||||
Active Internet connections (only servers)
|
||||
Proto Recv-Q Send-Q Local Address Foreign Address State
|
||||
tcp 0 0 0.0.0.0:2222 0.0.0.0:* LISTEN
|
||||
tcp 0 0 0.0.0.0:22169 0.0.0.0:* LISTEN
|
||||
tcp 0 0 0.0.0.0:8443 0.0.0.0:* LISTEN
|
||||
|
||||
Proxy mode is intended to be a developer tool, most useful in situations where root
|
||||
privileges are not available.
|
||||
|
||||
## DNS Server
|
||||
ziti-tunnel runs an internal DNS server by default. The DNS server must be first in the host's resolver
|
||||
configuration (e.g. resolve.conf). A self-test is performed when ziti-tunnel starts to ensure that its
|
||||
internal DNS server is configured in the system resolver:
|
||||
|
||||
INFO[0002] dns server started on 127.0.0.1:53
|
||||
INFO[0002] adding ziti-tunnel.resolver.test -> 19.65.28.94 to resolver
|
||||
INFO[0002] removing ziti-tunnel.resolver.test from resolver
|
||||
|
||||
The test involves inserting a known hostname/IP address into the internal DNS server, and using the system
|
||||
resolver to retrieve the address of the hostname. *ziti-tunnel will exit if the DNS self-test fails.*
|
||||
|
||||
Linux distributions typically manage the contents of /etc/resolv.conf, so simply editing the file
|
||||
will only work for a short time until /etc/resolv.conf is overwritten by the managing process.
|
||||
|
||||
Resolver configuration changes must survive restarts of the Linux name resolution manager. Linux
|
||||
distrubutions use one of several name resolution managers. The simplest way to determine which name
|
||||
resolution manager is being used by your Linux distrubtion is to look at /etc/resolv.conf:
|
||||
|
||||
$ ls -l /etc/resolv.conf
|
||||
|
||||
* If /etc/resolv.conf is a regular file, then it is most likely being managed by `dhclient`.
|
||||
* If /etc/resolv.conf is a symlink to a file in /run/systemd/resolve, then it is being
|
||||
managed by `systemd-resolved`
|
||||
|
||||
#### dhclient
|
||||
|
||||
If your Linux distribution uses dhclient, you can configure the system resolver to use
|
||||
ziti-tunnel's internal DNS server first by adding the following to /etc/dhcp/dhclient.conf:
|
||||
|
||||
prepend domain-name-servers 127.0.0.1;
|
||||
|
||||
Then restart network manager. Unless you know the name of the NetworkManager systemd
|
||||
service on your Linux distrubtion, it's probably easiest to reboot the host.
|
||||
|
||||
#### systemd-resolved
|
||||
|
||||
$ sudo ln -sf /run/systemd/resolve/resolv.conf /etc
|
||||
$ echo -e "[Resolve]\nDNS=127.0.0.1" | sudo tee /etc/systemd/resolved.conf.d/ziti-tunnel.conf
|
||||
$ sudo systemctl restart systemd-resolved
|
||||
|
||||
If you are unable to control the resolver on your operating system, ziti-tunnel can use/update a hosts file for
|
||||
any hostnames that it tunnels:
|
||||
|
||||
ziti-tunnel run --resolver file:///etc/hosts "${HOME}/ziti.json"
|
||||
|
||||
### IP Address Assignment
|
||||
|
||||
If the service specifies a hostname for its address, ziti-tunnel resolves the hostname and adds the result to its
|
||||
internal DNS server:
|
||||
|
||||
[0127] INFO adding myservice.mydomain.com -> 45.60.32.165 to resolver
|
||||
|
||||
If the service hostname does not resolve, ziti-tunnel will find an unused link-local address and assign it to
|
||||
the route for the service:
|
||||
|
||||
[0012] INFO adding bogushost.net -> 169.254.1.4 to resolver
|
||||
[0012] INFO ziti/tunnel/protocols/tcp.Listen: Accepting on 169.254.1.4:25 service=telnet
|
||||
@@ -0,0 +1,3 @@
|
||||
IDENTITY_JSON=/opt/netfoundry/etc/ziti-client-identity.json
|
||||
RESOLVER_OPTS=none
|
||||
INTERCEPT_MODE=tproxy
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) 2017-2019 NetFoundry, Inc.
|
||||
# All rights reserved.
|
||||
# https://netfoundry.io
|
||||
|
||||
|
||||
[Unit]
|
||||
Description=NetFoundry Ziti Tunnel
|
||||
After=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/systemd/system/ziti-tunnel.env
|
||||
ExecStart=/opt/netfoundry/bin/ziti-tunnel --identity "${IDENTITY_JSON}" --resolver "${RESOLVER_OPTS}" ${INTERCEPT_MODE}
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
|
||||
[Install]
|
||||
WantedBy=network-online.target
|
||||
|
||||
# capabilities don't work for tproxy, because it shells out to `iptables` which does things
|
||||
# that require CAP_NET_RAW. Unfortunately (for us), CAP_NET_RAW is not in the inheritable
|
||||
# capability set of /sbin/iptables.
|
||||
#CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
|
||||
#ProtectSystem=full
|
||||
#ProtectHome=true
|
||||
#ProtectKernelTunables=true
|
||||
#ProtectKernelModules=true
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# ziti-tunnel Start up the ziti-tunnel client
|
||||
#
|
||||
#
|
||||
# chkconfig: 345 28 72
|
||||
# descripttion: Intercept network traffic for NetFoundry/Ziti services.
|
||||
|
||||
### BEGIN INIT INFO
|
||||
# Provides: ziti-tunnel
|
||||
# Required-Start: $local_fs $network
|
||||
# Required-Stop:
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: Start Ziti Tunnel
|
||||
### END INIT INFO
|
||||
|
||||
ziti_dir=/opt/netfoundry/ziti
|
||||
service=$(basename $(readlink -f "${0}"))
|
||||
logfile="/var/log/${service}.log"
|
||||
pidfile="/var/run/${service}.pid"
|
||||
cfgfile="${ziti_dir}/cfg/${service}.json"
|
||||
jwtfile="${ziti_dir}/cfg/${service}.jwt"
|
||||
|
||||
# source function library
|
||||
. /etc/init.d/functions
|
||||
|
||||
# pull in sysconfig settings
|
||||
[ -f "/etc/sysconfig/${service}" ] && . "/etc/sysconfig/${service}"
|
||||
|
||||
|
||||
do_enroll()
|
||||
{
|
||||
if [ ! -f "${cfgfile}" ]; then
|
||||
echo -n $"Enrolling ${service}: "
|
||||
if [ -f "${jwtfile}" ]; then
|
||||
"${ziti_dir}/bin/ziti-enroller" --jwt "${jwtfile}" --out "${cfgfile}" >> "${logfile}" 2>&1
|
||||
if [ ${?} -ne 0 -o ! -f "${cfgfile}" ]; then
|
||||
failure $"Enrolling ${service}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "${cfgfile} does not exist, nor does ${jwtfile}" >> "${logfile}"
|
||||
failure $"Enrolling ${service}"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
success $"Enrolling ${service}"
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
do_start()
|
||||
{
|
||||
if [ "${VERBOSE}" = "YES" ]; then
|
||||
verbose_opts="--verbose"
|
||||
fi
|
||||
if [ -n "${RESOLVER}" ]; then
|
||||
resolver_opts="--resolver ${RESOLVER}"
|
||||
fi
|
||||
|
||||
do_enroll
|
||||
echo -n $"Starting ${service}: "
|
||||
("${ziti_dir}/bin/ziti-tunnel" run ${verbose_opts} ${resolver_opts} "${cfgfile}" >> "${logfile}" 2>&1 & echo "${!}" > "${pidfile}" && sleep 1; ps -p "${!}" > /dev/null)
|
||||
ec=${?}
|
||||
if [ ${ec} -eq 0 ]; then
|
||||
success $"logging to ${logfile}"
|
||||
else
|
||||
failure $"see ${logfile}"
|
||||
fi
|
||||
echo ""
|
||||
return ${ec}
|
||||
}
|
||||
|
||||
do_stop()
|
||||
{
|
||||
echo -n $"Stopping ${service}: "
|
||||
killproc -p "${pidfile}" "${ziti_dir}/bin/ziti-tunnel"
|
||||
echo ""
|
||||
}
|
||||
|
||||
do_status()
|
||||
{
|
||||
status -p "${pidfile}" "${service}"
|
||||
}
|
||||
|
||||
do_status_q()
|
||||
{
|
||||
do_status > /dev/null 2>&1
|
||||
}
|
||||
|
||||
case "${1}" in
|
||||
start)
|
||||
do_status_q && exit 0
|
||||
do_start
|
||||
;;
|
||||
stop)
|
||||
do_status_q && do_stop
|
||||
;;
|
||||
restart)
|
||||
do_stop
|
||||
sleep 1
|
||||
do_start
|
||||
;;
|
||||
status)
|
||||
do_status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: /etc/init.d/ziti-tunnel {start|stop|restart}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"net"
|
||||
"os"
|
||||
"fmt"
|
||||
"bufio"
|
||||
)
|
||||
|
||||
const hostFormat = "%s\t%s\t# NetFoundry"
|
||||
|
||||
type hostFile struct {
|
||||
path string
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewHostFile(path string) Resolver {
|
||||
return &hostFile{
|
||||
path: path,
|
||||
mutex: sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
func isHostPresent(hostname string, ip net.IP, f *os.File) bool {
|
||||
scanner := bufio.NewScanner(f)
|
||||
match := fmt.Sprintf(hostFormat, ip.String(), hostname)
|
||||
for scanner.Scan() {
|
||||
if scanner.Text() == match {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h hostFile) AddHostname(hostname string, ip net.IP) error {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
f, err := os.OpenFile(h.path, os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if !isHostPresent(hostname, ip, f) {
|
||||
_, err := f.Seek(0, os.SEEK_END)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(f, hostFormat+"\n", ip.String(), hostname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h hostFile) RemoveHostname(s string, ip net.IP) error {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h hostFile) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package dns
|
||||
|
||||
import "net"
|
||||
|
||||
type Resolver interface{
|
||||
AddHostname(string, net.IP) error
|
||||
RemoveHostname(string, net.IP) error
|
||||
Cleanup() error
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/sirupsen/logrus"
|
||||
"net"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var log = logrus.StandardLogger()
|
||||
|
||||
type resolver struct {
|
||||
server *dns.Server
|
||||
names map[string]net.IP
|
||||
namesMtx sync.Mutex
|
||||
}
|
||||
|
||||
func NewResolver(config string) Resolver {
|
||||
if config == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
resolverURL, err := url.Parse(config)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to parse resolver configuration '%s': %s", config, err)
|
||||
}
|
||||
|
||||
switch resolverURL.Scheme {
|
||||
case "", "file":
|
||||
return NewHostFile(resolverURL.Path)
|
||||
case "udp":
|
||||
return NewDnsServer(resolverURL.Host)
|
||||
}
|
||||
|
||||
log.Fatalf("invalid resolver configuration '%s'. must be 'file://' or 'udp://' URL", config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewDnsServer(addr string) Resolver {
|
||||
log.Infof("starting dns server...")
|
||||
s := &dns.Server{
|
||||
Addr: addr,
|
||||
Net: "udp",
|
||||
}
|
||||
|
||||
errChan := make(chan error)
|
||||
go func() {
|
||||
errChan <- s.ListenAndServe()
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-errChan:
|
||||
if err != nil {
|
||||
log.Fatalf("dns server failed to start: %s", err)
|
||||
} else {
|
||||
log.Fatal("dns server stopped prematurely")
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
log.Infof("dns server running at %s", s.Addr)
|
||||
}
|
||||
|
||||
names := make(map[string]net.IP)
|
||||
r := &resolver{server: s, names: names, namesMtx: sync.Mutex{}}
|
||||
s.Handler = r
|
||||
|
||||
const resolverConfigHelp = "ziti-tunnel runs an internal DNS server which must be first in the host's\n" +
|
||||
"resolver configuration. On systems that use NetManager/dhclient, this can\n" +
|
||||
"be achieved by adding the following to /etc/dhcp/dhclient.conf:\n" +
|
||||
"\n" +
|
||||
" prepend domain-name-servers %s;\n\n"
|
||||
|
||||
err := r.testSystemResolver()
|
||||
if err != nil {
|
||||
r.Cleanup()
|
||||
log.Fatalf("system resolver test failed: %s\n\n"+resolverConfigHelp, err, addr)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *resolver) testSystemResolver() error {
|
||||
const resolverTestHostname = "ziti-tunnel.resolver.test"
|
||||
resolverTestIP := net.IP{19, 65, 28, 94}
|
||||
log.Debug("testing system resolver configuration")
|
||||
|
||||
err := r.AddHostname(resolverTestHostname, resolverTestIP)
|
||||
if err != nil {
|
||||
return errors.New("failed to add self-test hostname")
|
||||
}
|
||||
|
||||
resolved, err := net.ResolveIPAddr("ip", resolverTestHostname)
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf("failed to resolve %s: %s", resolverTestHostname, err))
|
||||
}
|
||||
|
||||
// resolverTestIP = net.IP{19, 65, 28, 96} // force test failure by uncommenting
|
||||
if !resolved.IP.Equal(resolverTestIP) {
|
||||
return errors.New(fmt.Sprintf("unexpected resolved address %s", resolved.IP.String()))
|
||||
}
|
||||
|
||||
r.RemoveHostname(resolverTestHostname, resolverTestIP)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolver) ServeDNS(w dns.ResponseWriter, query *dns.Msg) {
|
||||
log.Debugf("received:\n%s\n", query.String())
|
||||
msg := dns.Msg{}
|
||||
msg.SetReply(query)
|
||||
msg.RecursionAvailable = false
|
||||
msg.Rcode = dns.RcodeNotImplemented
|
||||
q := query.Question[0]
|
||||
switch q.Qtype {
|
||||
case dns.TypeA:
|
||||
name := q.Name
|
||||
address, ok := r.names[name]
|
||||
if ok {
|
||||
msg.Authoritative = true
|
||||
msg.Rcode = dns.RcodeSuccess
|
||||
answer := &dns.A{
|
||||
Hdr: dns.RR_Header{Name: name, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 60},
|
||||
A: address,
|
||||
}
|
||||
msg.Answer = append(msg.Answer, answer)
|
||||
} else {
|
||||
msg.Rcode = dns.RcodeRefused // fail fast, and inspire resolver to query next name server in its list.
|
||||
}
|
||||
}
|
||||
log.Debugf("response:\n%s\n", msg.String())
|
||||
err := w.WriteMsg(&msg)
|
||||
if err != nil {
|
||||
log.Errorf("write failed: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (r *resolver) AddHostname(hostname string, ip net.IP) error {
|
||||
r.namesMtx.Lock()
|
||||
log.Infof("adding %s = %s to resolver", hostname, ip.String())
|
||||
r.names[hostname+"."] = ip
|
||||
r.namesMtx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolver) RemoveHostname(hostname string, ip net.IP) error {
|
||||
r.namesMtx.Lock()
|
||||
if _, ok := r.names[hostname+"."]; ok {
|
||||
log.Infof("removing %s from resolver", hostname)
|
||||
}
|
||||
delete(r.names, hostname+".")
|
||||
r.namesMtx.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *resolver) Cleanup() error {
|
||||
log.Debug("shutting down")
|
||||
return r.server.Shutdown()
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package intercept
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/dns"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Protocol int
|
||||
|
||||
const (
|
||||
TCP Protocol = 2
|
||||
UDP Protocol = 17
|
||||
)
|
||||
|
||||
type Interceptor interface {
|
||||
Start(context ziti.Context)
|
||||
Stop()
|
||||
Intercept(service edge.Service, resolver dns.Resolver) error
|
||||
StopIntercepting(serviceName string, removeRoute bool) error
|
||||
}
|
||||
|
||||
// Interceptors need to maintain state for services that are being intercepted.
|
||||
// Intercepted services need to be looked up by:
|
||||
// - intercepted address - this is all an interceptor knows when an inbound connection is made
|
||||
// - service name - when a service is removed (e.g. from an appwan)
|
||||
|
||||
type InterceptedService struct {
|
||||
Name string
|
||||
Addr interceptAddress
|
||||
Data interface{} // interceptor-specific data attached to this intercept. intent is to provide context needed in Interceptor.StopIntercepting
|
||||
}
|
||||
|
||||
type interceptAddress struct {
|
||||
cidr string
|
||||
port int
|
||||
protocol string
|
||||
}
|
||||
|
||||
func (addr interceptAddress) Proto() string {
|
||||
return addr.protocol
|
||||
}
|
||||
|
||||
func (addr interceptAddress) IpNet() net.IPNet {
|
||||
_, ipNet, err := net.ParseCIDR(addr.cidr)
|
||||
if err != nil {
|
||||
pfxlog.Logger().Errorf("net.ParseCIDR(%s) failed: %v", addr.cidr, err)
|
||||
// TODO return error? nil?
|
||||
}
|
||||
return *ipNet
|
||||
}
|
||||
|
||||
func (addr interceptAddress) Port() int {
|
||||
return addr.port
|
||||
}
|
||||
|
||||
// Return the length of a full prefix (no subnetting) for the given IP address.
|
||||
// Returns 32 for ipv4 addresses, and 128 for ipv6 addresses.
|
||||
func addrBits(ip net.IP) int {
|
||||
if ip == nil {
|
||||
return 0
|
||||
} else if ip.To4() != nil {
|
||||
return net.IPv4len * 8
|
||||
} else if ip.To16() != nil {
|
||||
return net.IPv6len * 8
|
||||
}
|
||||
|
||||
pfxlog.Logger().Infof("invalid IP address %s", ip.String())
|
||||
return 0
|
||||
}
|
||||
|
||||
func NewInterceptAddress(service edge.Service, protocol string, resolver dns.Resolver) (*interceptAddress, error) {
|
||||
ip, err := getInterceptIP(service.Dns.Hostname, resolver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get intercept IP address: %v", err)
|
||||
}
|
||||
|
||||
prefixLen := addrBits(ip)
|
||||
ipNet := net.IPNet{IP: ip, Mask: net.CIDRMask(prefixLen, prefixLen)}
|
||||
addr := interceptAddress{cidr: ipNet.String(), port: service.Dns.Port, protocol: protocol}
|
||||
return &addr, nil
|
||||
}
|
||||
|
||||
type LUT map[interceptAddress]InterceptedService
|
||||
|
||||
func NewLUT() LUT {
|
||||
return make(map[interceptAddress]InterceptedService)
|
||||
}
|
||||
|
||||
// Return the service that is being intercepted at the given address, if any.
|
||||
func (m LUT) GetByAddress(addr net.Addr) (*InterceptedService, error) {
|
||||
//var proto string
|
||||
var ip net.IP
|
||||
var port int
|
||||
var protocol string
|
||||
switch addrType := addr.(type) {
|
||||
case *net.TCPAddr:
|
||||
ip = addrType.IP
|
||||
port = addrType.Port
|
||||
protocol = "tcp"
|
||||
case *net.UDPAddr:
|
||||
ip = addrType.IP
|
||||
port = addrType.Port
|
||||
protocol = "udp"
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported address type: %v", addrType)
|
||||
}
|
||||
|
||||
prefixLen := addrBits(ip)
|
||||
ipNet := &net.IPNet{IP: ip, Mask: net.CIDRMask(prefixLen, prefixLen)}
|
||||
cidr := ipNet.String()
|
||||
|
||||
s, ok := m[interceptAddress{cidr, port, protocol}]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("address %s is not being intercepted", addr.String())
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
func (m LUT) GetByName(serviceName string) (*InterceptedService, error) {
|
||||
for _, service := range m {
|
||||
if serviceName == service.Name {
|
||||
return &service, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("service %s is not being intercepted", serviceName)
|
||||
}
|
||||
|
||||
func (m LUT) Put(addr interceptAddress, serviceName string, data interface{}) error {
|
||||
m[addr] = InterceptedService{
|
||||
Name: serviceName,
|
||||
Addr: addr,
|
||||
Data: data,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m LUT) Remove(interceptAddr interceptAddress) {
|
||||
delete(m, interceptAddr)
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package intercept
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/tunnel/dns"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/utils"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"net"
|
||||
)
|
||||
|
||||
func getInterceptIP(hostname string, resolver dns.Resolver) (net.IP, error) {
|
||||
log := pfxlog.Logger()
|
||||
// hostname is an ip address, return it
|
||||
parsedIP := net.ParseIP(hostname)
|
||||
if parsedIP != nil {
|
||||
return parsedIP, nil
|
||||
}
|
||||
|
||||
// - apparently not an IP address, assume hostname and attempt lookup:
|
||||
// - use first result if any
|
||||
// - pin first IP to hostname in /etc/hosts
|
||||
addrs, err := net.LookupIP(hostname)
|
||||
if err == nil {
|
||||
if len(addrs) > 0 {
|
||||
resolver.AddHostname(hostname, addrs[0].To4())
|
||||
return addrs[0], nil
|
||||
}
|
||||
} else {
|
||||
log.Debugf("net.LookupIp(%s) failed: %s", hostname, err)
|
||||
}
|
||||
|
||||
ip, err := utils.NextIP(net.IP{169, 254, 1, 1}, net.IP{169, 254, 254, 254})
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("invalid IP address or unresolvable hostname: %s", hostname)
|
||||
}
|
||||
resolver.AddHostname(hostname, ip)
|
||||
return ip, nil
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
This directory contains protocol implementations intended for use by interceptors
|
||||
that need to decode/encode underlay packets.
|
||||
|
||||
Currently only the `tun` interceptor requires protocol support.
|
||||
`tptoxy` and `proxy` interceptors deal with sockets instead of packets, and
|
||||
rely on the Linux kernel to handle the underlay protocol details.
|
||||
@@ -0,0 +1,59 @@
|
||||
// lifted from https://github.com/google/netstack/blob/master/tcpip/header/checksum.go
|
||||
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// 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.
|
||||
|
||||
// Package header provides the implementation of the encoding and decoding of
|
||||
// network protocol headers.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// Checksum calculates the checksum (as defined in RFC 1071) of the bytes in the
|
||||
// given byte array.
|
||||
func Checksum(buf []byte, initial uint16) uint16 {
|
||||
v := uint32(initial)
|
||||
|
||||
l := len(buf)
|
||||
if l&1 != 0 {
|
||||
l--
|
||||
v += uint32(buf[l]) << 8
|
||||
}
|
||||
|
||||
for i := 0; i < l; i += 2 {
|
||||
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
|
||||
}
|
||||
|
||||
return ChecksumCombine(uint16(v), uint16(v>>16))
|
||||
}
|
||||
|
||||
// ChecksumCombine combines the two uint16 to form their checksum. This is done
|
||||
// by adding them and the carry.
|
||||
func ChecksumCombine(a, b uint16) uint16 {
|
||||
v := uint32(a) + uint32(b)
|
||||
return uint16(v + v>>16)
|
||||
}
|
||||
|
||||
// PseudoHeaderChecksum calculates the pseudo-header checksum for the
|
||||
// given destination protocol and network address, ignoring the length
|
||||
// field. Pseudo-headers are needed by transport layers when calculating
|
||||
// their own checksum.
|
||||
func PseudoHeaderChecksum(protocol int, srcAddr net.IP, dstAddr net.IP) uint16 {
|
||||
xsum := Checksum([]byte(srcAddr), 0)
|
||||
xsum = Checksum([]byte(dstAddr), xsum)
|
||||
return Checksum([]byte{0, uint8(protocol)}, xsum)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Parse a layer 3 packet (ipv4 or ipv6), and return
|
||||
// - src, dst IP addresses,
|
||||
// - layer 4 protocol
|
||||
// - payload
|
||||
// - error
|
||||
func Decode(packet []byte) (net.IP, net.IP, int, []byte, error) {
|
||||
var l4Proto int
|
||||
var payload []byte
|
||||
var src, dst net.IP
|
||||
|
||||
ipVersion := packet[0] >> 4
|
||||
switch ipVersion {
|
||||
case 4:
|
||||
h := IPv4(packet)
|
||||
src = h.SourceAddress()
|
||||
dst = h.DestinationAddress()
|
||||
l4Proto = int(h.Protocol())
|
||||
payload = h.Payload()
|
||||
default:
|
||||
return nil, nil, 0, nil, errors.New(fmt.Sprintf("unsupported IP version %d", ipVersion))
|
||||
}
|
||||
|
||||
return src, dst, l4Proto, payload, nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// lifted from https://github.com/google/netstack/blob/master/tcpip/header/ipv4.go
|
||||
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// 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.
|
||||
|
||||
package ip
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
versIHL = 0
|
||||
tos = 1
|
||||
totalLen = 2
|
||||
id = 4
|
||||
flagsFO = 6
|
||||
ttl = 8
|
||||
protocol = 9
|
||||
checksum = 10
|
||||
srcAddr = 12
|
||||
dstAddr = 16
|
||||
)
|
||||
|
||||
// IPv4Fields contains the fields of an IPv4 packet. It is used to describe the
|
||||
// fields of a packet that needs to be encoded.
|
||||
type IPv4Fields struct {
|
||||
// IHL is the "internet header length" field of an IPv4 packet.
|
||||
IHL uint8
|
||||
|
||||
// TOS is the "type of service" field of an IPv4 packet.
|
||||
TOS uint8
|
||||
|
||||
// TotalLength is the "total length" field of an IPv4 packet.
|
||||
TotalLength uint16
|
||||
|
||||
// ID is the "identification" field of an IPv4 packet.
|
||||
ID uint16
|
||||
|
||||
// Flags is the "flags" field of an IPv4 packet.
|
||||
Flags uint8
|
||||
|
||||
// FragmentOffset is the "fragment offset" field of an IPv4 packet.
|
||||
FragmentOffset uint16
|
||||
|
||||
// TTL is the "time to live" field of an IPv4 packet.
|
||||
TTL uint8
|
||||
|
||||
// Protocol is the "protocol" field of an IPv4 packet.
|
||||
Protocol uint8
|
||||
|
||||
// Checksum is the "checksum" field of an IPv4 packet.
|
||||
Checksum uint16
|
||||
|
||||
// SrcAddr is the "source ip address" of an IPv4 packet.
|
||||
SrcAddr net.IP
|
||||
|
||||
// DstAddr is the "destination ip address" of an IPv4 packet.
|
||||
DstAddr net.IP
|
||||
}
|
||||
|
||||
// IPv4 represents an ipv4 header stored in a byte array.
|
||||
// Most of the methods of IPv4 access to the underlying slice without
|
||||
// checking the boundaries and could panic because of 'index out of range'.
|
||||
// Always call IsValid() to validate an instance of IPv4 before using other methods.
|
||||
type IPv4 []byte
|
||||
|
||||
const (
|
||||
// IPv4MinimumSize is the minimum size of a valid IPv4 packet.
|
||||
IPv4MinimumSize = 20
|
||||
|
||||
// IPv4MaximumHeaderSize is the maximum size of an IPv4 header. Given
|
||||
// that there are only 4 bits to represents the header length in 32-bit
|
||||
// units, the header cannot exceed 15*4 = 60 bytes.
|
||||
IPv4MaximumHeaderSize = 60
|
||||
|
||||
// IPv4AddressSize is the size, in bytes, of an IPv4 address.
|
||||
IPv4AddressSize = 4
|
||||
|
||||
// IPv4ProtocolNumber is IPv4's network protocol number.
|
||||
IPv4ProtocolNumber uint32 = 0x0800
|
||||
|
||||
// IPv4Version is the version of the ipv4 protocol.
|
||||
IPv4Version = 4
|
||||
|
||||
// IPv4Broadcast is the broadcast address of the IPv4 procotol.
|
||||
//IPv4Broadcast tcpip.Address = "\xff\xff\xff\xff"
|
||||
|
||||
// IPv4Any is the non-routable IPv4 "any" meta address.
|
||||
//IPv4Any tcpip.Address = "\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// Flags that may be set in an IPv4 packet.
|
||||
const (
|
||||
IPv4FlagMoreFragments = 1 << iota
|
||||
IPv4FlagDontFragment
|
||||
)
|
||||
|
||||
// IPVersion returns the version of IP used in the given packet. It returns -1
|
||||
// if the packet is not large enough to contain the version field.
|
||||
func IPVersion(b []byte) int {
|
||||
// Length must be at least offset+length of version field.
|
||||
if len(b) < versIHL+1 {
|
||||
return -1
|
||||
}
|
||||
return int(b[versIHL] >> 4)
|
||||
}
|
||||
|
||||
// HeaderLength returns the value of the "header length" field of the ipv4
|
||||
// header.
|
||||
func (b IPv4) HeaderLength() uint8 {
|
||||
return (b[versIHL] & 0xf) * 4
|
||||
}
|
||||
|
||||
// ID returns the value of the identifier field of the ipv4 header.
|
||||
func (b IPv4) ID() uint16 {
|
||||
return binary.BigEndian.Uint16(b[id:])
|
||||
}
|
||||
|
||||
// Protocol returns the value of the protocol field of the ipv4 header.
|
||||
func (b IPv4) Protocol() uint8 {
|
||||
return b[protocol]
|
||||
}
|
||||
|
||||
// Flags returns the "flags" field of the ipv4 header.
|
||||
func (b IPv4) Flags() uint8 {
|
||||
return uint8(binary.BigEndian.Uint16(b[flagsFO:]) >> 13)
|
||||
}
|
||||
|
||||
// TTL returns the "TTL" field of the ipv4 header.
|
||||
func (b IPv4) TTL() uint8 {
|
||||
return b[ttl]
|
||||
}
|
||||
|
||||
// FragmentOffset returns the "fragment offset" field of the ipv4 header.
|
||||
func (b IPv4) FragmentOffset() uint16 {
|
||||
return binary.BigEndian.Uint16(b[flagsFO:]) << 3
|
||||
}
|
||||
|
||||
// TotalLength returns the "total length" field of the ipv4 header.
|
||||
func (b IPv4) TotalLength() uint16 {
|
||||
return binary.BigEndian.Uint16(b[totalLen:])
|
||||
}
|
||||
|
||||
// Checksum returns the checksum field of the ipv4 header.
|
||||
func (b IPv4) Checksum() uint16 {
|
||||
return binary.BigEndian.Uint16(b[checksum:])
|
||||
}
|
||||
|
||||
// SourceAddress returns the "source address" field of the ipv4 header.
|
||||
func (b IPv4) SourceAddress() net.IP {
|
||||
return net.IP(b[srcAddr : srcAddr+IPv4AddressSize])
|
||||
}
|
||||
|
||||
// DestinationAddress returns the "destination address" field of the ipv4
|
||||
// header.
|
||||
func (b IPv4) DestinationAddress() net.IP {
|
||||
return net.IP(b[dstAddr : dstAddr+IPv4AddressSize])
|
||||
}
|
||||
|
||||
// TransportProtocol implements Network.TransportProtocol.
|
||||
func (b IPv4) TransportProtocol() uint8 {
|
||||
return b.Protocol()
|
||||
}
|
||||
|
||||
// Payload implements Network.Payload.
|
||||
func (b IPv4) Payload() []byte {
|
||||
return b[b.HeaderLength():][:b.PayloadLength()]
|
||||
}
|
||||
|
||||
// PayloadLength returns the length of the payload portion of the ipv4 packet.
|
||||
func (b IPv4) PayloadLength() uint16 {
|
||||
return b.TotalLength() - uint16(b.HeaderLength())
|
||||
}
|
||||
|
||||
// TOS returns the "type of service" field of the ipv4 header.
|
||||
func (b IPv4) TOS() (uint8, uint32) {
|
||||
return b[tos], 0
|
||||
}
|
||||
|
||||
// SetTOS sets the "type of service" field of the ipv4 header.
|
||||
func (b IPv4) SetTOS(v uint8, _ uint32) {
|
||||
b[tos] = v
|
||||
}
|
||||
|
||||
// SetTotalLength sets the "total length" field of the ipv4 header.
|
||||
func (b IPv4) SetTotalLength(totalLength uint16) {
|
||||
binary.BigEndian.PutUint16(b[totalLen:], totalLength)
|
||||
}
|
||||
|
||||
// SetChecksum sets the checksum field of the ipv4 header.
|
||||
func (b IPv4) SetChecksum(v uint16) {
|
||||
binary.BigEndian.PutUint16(b[checksum:], v)
|
||||
}
|
||||
|
||||
// SetFlagsFragmentOffset sets the "flags" and "fragment offset" fields of the
|
||||
// ipv4 header.
|
||||
func (b IPv4) SetFlagsFragmentOffset(flags uint8, offset uint16) {
|
||||
v := (uint16(flags) << 13) | (offset >> 3)
|
||||
binary.BigEndian.PutUint16(b[flagsFO:], v)
|
||||
}
|
||||
|
||||
// SetID sets the identification field.
|
||||
func (b IPv4) SetID(v uint16) {
|
||||
binary.BigEndian.PutUint16(b[id:], v)
|
||||
}
|
||||
|
||||
// SetSourceAddress sets the "source address" field of the ipv4 header.
|
||||
func (b IPv4) SetSourceAddress(addr net.IP) {
|
||||
copy(b[srcAddr:srcAddr+IPv4AddressSize], addr)
|
||||
}
|
||||
|
||||
// SetDestinationAddress sets the "destination address" field of the ipv4
|
||||
// header.
|
||||
func (b IPv4) SetDestinationAddress(addr net.IP) {
|
||||
copy(b[dstAddr:dstAddr+IPv4AddressSize], addr)
|
||||
}
|
||||
|
||||
// CalculateChecksum calculates the checksum of the ipv4 header.
|
||||
func (b IPv4) CalculateChecksum() uint16 {
|
||||
return Checksum(b[:b.HeaderLength()], 0)
|
||||
}
|
||||
|
||||
// Encode encodes all the fields of the ipv4 header.
|
||||
func (b IPv4) Encode(i *IPv4Fields) {
|
||||
b[versIHL] = (4 << 4) | ((i.IHL / 4) & 0xf)
|
||||
b[tos] = i.TOS
|
||||
b.SetTotalLength(i.TotalLength)
|
||||
binary.BigEndian.PutUint16(b[id:], i.ID)
|
||||
b.SetFlagsFragmentOffset(i.Flags, i.FragmentOffset)
|
||||
b[ttl] = i.TTL
|
||||
b[protocol] = i.Protocol
|
||||
b.SetChecksum(i.Checksum)
|
||||
copy(b[srcAddr:srcAddr+IPv4AddressSize], i.SrcAddr)
|
||||
copy(b[dstAddr:dstAddr+IPv4AddressSize], i.DstAddr)
|
||||
}
|
||||
|
||||
// EncodePartial updates the total length and checksum fields of ipv4 header,
|
||||
// taking in the partial checksum, which is the checksum of the header without
|
||||
// the total length and checksum fields. It is useful in cases when similar
|
||||
// packets are produced.
|
||||
func (b IPv4) EncodePartial(partialChecksum, totalLength uint16) {
|
||||
b.SetTotalLength(totalLength)
|
||||
checksum := Checksum(b[totalLen:totalLen+2], partialChecksum)
|
||||
b.SetChecksum(^checksum)
|
||||
}
|
||||
|
||||
// IsValid performs basic validation on the packet.
|
||||
func (b IPv4) IsValid(pktSize int) bool {
|
||||
if len(b) < IPv4MinimumSize {
|
||||
return false
|
||||
}
|
||||
|
||||
hlen := int(b.HeaderLength())
|
||||
tlen := int(b.TotalLength())
|
||||
if hlen > tlen || tlen > pktSize {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// IsV4MulticastAddress determines if the provided address is an IPv4 multicast
|
||||
// address (range 224.0.0.0 to 239.255.255.255). The four most significant bits
|
||||
// will be 1110 = 0xe0.
|
||||
func IsV4MulticastAddress(addr net.IP) bool {
|
||||
if len(addr) != IPv4AddressSize {
|
||||
return false
|
||||
}
|
||||
return (addr[0] & 0xf0) == 0xe0
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package protocols
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/ip"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/tcp"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/udp"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/udp_vconn"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
)
|
||||
|
||||
type rxPacket struct {
|
||||
data []byte
|
||||
release func()
|
||||
}
|
||||
|
||||
// Read packets from the tun interface
|
||||
func HandleInboundPackets(context ziti.Context, dev io.ReadWriter, mtu uint, udpManager *udp.TunUDPManager) {
|
||||
log := pfxlog.Logger()
|
||||
|
||||
// initialize a buffer pool for incoming packets
|
||||
// TEST with small MTU
|
||||
rxq := make(chan *rxPacket, 16)
|
||||
for i := 0; i < cap(rxq); i++ {
|
||||
packet := rxPacket{
|
||||
data: make([]byte, mtu),
|
||||
}
|
||||
packet.release = func() {
|
||||
rxq <- &packet
|
||||
}
|
||||
rxq <- &packet
|
||||
}
|
||||
|
||||
vconnMgr := udp_vconn.NewManager(context, udp_vconn.NewUnlimitedConnectionPolicy(), udp_vconn.NewDefaultExpirationPolicy())
|
||||
|
||||
for {
|
||||
packet := <-rxq
|
||||
n, err := dev.Read(packet.data)
|
||||
if err != nil {
|
||||
log.Fatal("failed reading from tun: ", err)
|
||||
}
|
||||
|
||||
src, dst, proto, payload, err := ip.Decode(packet.data)
|
||||
if err != nil {
|
||||
log.Error("failed parsing layer 3 packet: ", err)
|
||||
}
|
||||
|
||||
log.WithFields(logrus.Fields{"src": src, "dst": dst, "proto": proto}).Debugf("read %d bytes", n)
|
||||
enqueued := false
|
||||
switch proto {
|
||||
case tcp.TCPProtocolNumber:
|
||||
enqueued = tcp.Enqueue(context, src, dst, payload, dev, mtu, packet.release)
|
||||
case udp.ProtocolNumber:
|
||||
log.Infof("Received udp packet %v, %v", src, dst)
|
||||
event := udpManager.CreateEvent(context, src, dst, payload, dev, packet.release)
|
||||
vconnMgr.QueueEvent(event)
|
||||
enqueued = true
|
||||
default:
|
||||
log.Errorf("protocol %d is not supported", proto)
|
||||
}
|
||||
|
||||
if !enqueued {
|
||||
log.Debug("dropping packet!") // TODO better logging
|
||||
packet.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/ip"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type tcpState uint16
|
||||
|
||||
const (
|
||||
tcpsClosed tcpState = iota // closed
|
||||
tcpsListen // listening for connection (passive open)
|
||||
tcpsSynSent // have sent SYN (active open)
|
||||
tcpsSynReceived // have sent and received SYN; awaiting ACK
|
||||
tcpsEstablished // established (data transfer)
|
||||
tcpsCloseWait // received FIN; waiting for application close
|
||||
tcpsFinWait1 // have closed, sent FUN; awaiting ACK and FIN
|
||||
tcpsClosing // simultaneous close; awaiting ACK
|
||||
tcpsLastAck // received FIN, have closed; awaiting ACK
|
||||
tcpsFinWait2 // have closed; awaiting FIN
|
||||
tcpsTimeWait // 2MSL (maximum segment lifetime) wait state after active close
|
||||
)
|
||||
|
||||
type ClientConn struct {
|
||||
net.Conn
|
||||
|
||||
clientKey string
|
||||
svcKey string
|
||||
srcAddr *net.TCPAddr // TODO encode addresses into template packet
|
||||
dstAddr *net.TCPAddr
|
||||
rxq chan *tcpQItem // inbound packets from peer
|
||||
txq chan []byte // queue of layer 3 packets for peer
|
||||
dev io.ReadWriter
|
||||
mss uint
|
||||
|
||||
tState tcpState
|
||||
rcvAdv uint16 // advertised window from peer - written in tcpRecv. read in tcpSend (waitForClientAcks)
|
||||
rcvWndScale uint8
|
||||
rcvAckNum uint32
|
||||
sndUna uint32 // oldest unacknowledged sequence number
|
||||
sndAckNum uint32 // bytes received. written in tcpRecv. read in tcpSend
|
||||
seqCond sync.Cond
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
type tcpSeq uint32
|
||||
|
||||
/* comparison functions to deal with sequence wrapping */
|
||||
func (n tcpSeq) lt(o tcpSeq) bool { return int32(n-o) < 0 }
|
||||
func (n tcpSeq) leq(o tcpSeq) bool { return int32(n-o) <= 0 }
|
||||
func (n tcpSeq) gt(o tcpSeq) bool { return int32(n-o) > 0 }
|
||||
func (n tcpSeq) geq(o tcpSeq) bool { return int32(n-o) >= 0 }
|
||||
|
||||
func NewClientConn(clientAddr, interceptAddr string, rxq chan *tcpQItem, dev io.ReadWriter, tunMTU uint) (*ClientConn, error) {
|
||||
srcAddr, err := net.ResolveTCPAddr("tcp", clientAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dstAddr, err := net.ResolveTCPAddr("tcp", interceptAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mss := tunMTU - (ip.IPv4MinimumSize + TCPMinimumSize)
|
||||
maxSegCond := sync.Cond{L: &sync.Mutex{}}
|
||||
txq := make(chan []byte, 16)
|
||||
for i := 0; i < cap(txq); i++ {
|
||||
txq <- make([]byte, tunMTU)
|
||||
}
|
||||
return &ClientConn{
|
||||
clientKey: clientAddr,
|
||||
svcKey: interceptAddr,
|
||||
srcAddr: srcAddr,
|
||||
dstAddr: dstAddr,
|
||||
rxq: rxq,
|
||||
txq: txq,
|
||||
dev: dev,
|
||||
mss: mss,
|
||||
tState: tcpsListen,
|
||||
seqCond: maxSegCond,
|
||||
log: log.WithFields(logrus.Fields{"src": srcAddr.String(), "dst": dstAddr.String()}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (conn *ClientConn) tcpRecv(segment TCP, buf []byte) (int, error) {
|
||||
conn.log.Debugf("got packet: flags=%d, state=%d", segment.Flags(), conn.tState)
|
||||
|
||||
conn.seqCond.L.Lock()
|
||||
conn.rcvAdv = segment.WindowSize()
|
||||
if segment.HasFlags(TCPFlagAck) {
|
||||
conn.rcvAckNum = segment.AckNumber()
|
||||
conn.seqCond.Signal()
|
||||
}
|
||||
|
||||
if segment.HasFlags(TCPFlagSyn) {
|
||||
conn.sndAckNum = segment.SequenceNumber() + 1 // +1 for syn received
|
||||
}
|
||||
if segment.HasFlags(TCPFlagFin) {
|
||||
conn.sndAckNum += 1
|
||||
}
|
||||
|
||||
switch {
|
||||
// LISTEN --> SYN_RCVD
|
||||
case conn.tState == tcpsListen && segment.HasFlags(TCPFlagSyn):
|
||||
synOpts := ParseSynOptions([]byte(segment[TCPMinimumSize:segment.DataOffset()]), segment.HasFlags(TCPFlagAck))
|
||||
if synOpts.WS > 0 {
|
||||
conn.rcvWndScale = uint8(synOpts.WS)
|
||||
}
|
||||
conn.tcpSend(nil, TCPFlagSyn|TCPFlagAck)
|
||||
conn.tState = tcpsSynReceived
|
||||
|
||||
// SYN_RCVD --> ESTABLISHED
|
||||
case conn.tState == tcpsSynReceived && segment.HasFlags(TCPFlagAck):
|
||||
conn.tState = tcpsEstablished
|
||||
|
||||
case conn.tState == tcpsEstablished:
|
||||
if segment.HasFlags(TCPFlagFin) {
|
||||
// CONNECTION TERMINATED by local application
|
||||
conn.tState = tcpsCloseWait
|
||||
conn.tcpSend(nil, TCPFlagAck)
|
||||
} else {
|
||||
// DATA TRANSFER
|
||||
n := len(segment.Payload())
|
||||
if n > 0 {
|
||||
conn.sndAckNum += uint32(n)
|
||||
conn.log.Debugf("writing %d bytes to ziti", n)
|
||||
conn.tcpSend(nil, TCPFlagAck)
|
||||
conn.seqCond.L.Unlock()
|
||||
copy(buf, segment.Payload())
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
|
||||
case conn.tState == tcpsLastAck && segment.HasFlags(TCPFlagAck):
|
||||
conn.tState = tcpsClosed
|
||||
|
||||
// CONNECTION TERMINATED by ziti service
|
||||
case conn.tState == tcpsFinWait1:
|
||||
switch {
|
||||
case segment.HasFlags(TCPFlagFin | TCPFlagAck):
|
||||
conn.tcpSend(nil, TCPFlagAck)
|
||||
conn.tState = tcpsTimeWait
|
||||
case segment.HasFlags(TCPFlagFin):
|
||||
conn.tcpSend(nil, TCPFlagAck)
|
||||
conn.tState = tcpsClosing
|
||||
case segment.HasFlags(TCPFlagAck):
|
||||
conn.tState = tcpsFinWait2
|
||||
}
|
||||
|
||||
case conn.tState == tcpsFinWait2 && segment.HasFlags(TCPFlagFin):
|
||||
conn.tcpSend(nil, TCPFlagAck)
|
||||
conn.tState = tcpsTimeWait
|
||||
|
||||
case conn.tState == tcpsClosing && segment.HasFlags(TCPFlagAck):
|
||||
conn.tState = tcpsTimeWait
|
||||
|
||||
case conn.tState == tcpsTimeWait && segment.HasFlags(TCPFlagFin):
|
||||
conn.tcpSend(nil, TCPFlagAck)
|
||||
|
||||
default:
|
||||
conn.log.Errorf("unexpected state transition %s/%d", conn.srcAddr, conn.tState)
|
||||
}
|
||||
|
||||
switch conn.tState {
|
||||
case tcpsClosed:
|
||||
conn.log.Warn("received packet on closed connection") // todo can this happen - yes - need to drain channel
|
||||
return 0, io.EOF
|
||||
case tcpsClosing:
|
||||
conn.log.Infof("closing %s", conn.clientKey)
|
||||
// TODO return 0, io.EOF?
|
||||
case tcpsCloseWait:
|
||||
conn.tState = tcpsLastAck
|
||||
conn.tcpSend(nil, TCPFlagFin)
|
||||
conn.seqCond.L.Unlock()
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
conn.seqCond.L.Unlock()
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Reads the next packet from the local client
|
||||
// implements server state transitions described in https://raw.githubusercontent.com/GordonMcKinney/gist-assets/master/TCPIP_State_Transition_Diagram.png
|
||||
func (conn *ClientConn) Read(buf []byte) (int, error) {
|
||||
segment, ok := <-conn.rxq
|
||||
if !ok {
|
||||
conn.log.Info("EOF while reading")
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err := conn.tcpRecv(segment.segment, buf)
|
||||
segment.release()
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Write to the local client.
|
||||
// seqNum is sequence of first payload byte for connection
|
||||
// acknum is bytes received from client
|
||||
func (conn *ClientConn) tcpSend(payload []byte, flags uint8) {
|
||||
tcpOpts := []byte(nil)
|
||||
if flags&TCPFlagSyn != 0 {
|
||||
synOpts := TCPSynOptions{
|
||||
MSS: uint16(conn.mss),
|
||||
WS: 7,
|
||||
}
|
||||
tcpOpts = makeSynOptions(synOpts)
|
||||
}
|
||||
optLen := len(tcpOpts)
|
||||
dataLen := len(payload)
|
||||
packet := <-conn.txq
|
||||
defer func() {
|
||||
packet = packet[:]
|
||||
conn.txq <- packet
|
||||
}()
|
||||
packet = packet[:ip.IPv4MinimumSize+TCPMinimumSize+optLen+dataLen]
|
||||
ipHeader := ip.IPv4(packet[0:ip.IPv4MinimumSize])
|
||||
ipHeader.Encode(&ip.IPv4Fields{
|
||||
IHL: ip.IPv4MinimumSize,
|
||||
TotalLength: uint16(len(packet)),
|
||||
Protocol: uint8(TCPProtocolNumber),
|
||||
SrcAddr: conn.dstAddr.IP.To4(),
|
||||
DstAddr: conn.srcAddr.IP.To4(),
|
||||
TTL: 64,
|
||||
})
|
||||
ipHeader.SetChecksum(^ipHeader.CalculateChecksum())
|
||||
tcpHeader := TCP(packet[ip.IPv4MinimumSize : ip.IPv4MinimumSize+TCPMinimumSize+optLen])
|
||||
tcpHeader.Encode(&TCPFields{
|
||||
SrcPort: uint16(conn.dstAddr.Port),
|
||||
DstPort: uint16(conn.srcAddr.Port),
|
||||
SeqNum: conn.sndUna + 1,
|
||||
AckNum: conn.sndAckNum,
|
||||
DataOffset: TCPMinimumSize + uint8(optLen),
|
||||
Flags: flags,
|
||||
WindowSize: 0xffff,
|
||||
})
|
||||
copy(packet[ip.IPv4MinimumSize+TCPMinimumSize:], tcpOpts)
|
||||
copy(packet[ip.IPv4MinimumSize+TCPMinimumSize+optLen:], payload)
|
||||
sum := ip.PseudoHeaderChecksum(TCPProtocolNumber, conn.dstAddr.IP.To4(), conn.srcAddr.IP.To4())
|
||||
sum = ip.Checksum(payload, sum)
|
||||
tcpHeader.SetChecksum(^tcpHeader.CalculateChecksum(sum, TCPMinimumSize+uint16(optLen+dataLen)))
|
||||
conn.log.Debugf("sending seqNum %d (len %d)", tcpHeader.SequenceNumber(), dataLen)
|
||||
nOut, err := conn.dev.Write(packet)
|
||||
if nOut != len(packet) {
|
||||
conn.log.Errorf("short write %d of %d bytes %v", nOut, len(packet), err)
|
||||
}
|
||||
inc := uint32(dataLen)
|
||||
if flags&(TCPFlagSyn|TCPFlagFin) != 0 {
|
||||
inc += 1
|
||||
}
|
||||
conn.sndUna += inc
|
||||
}
|
||||
|
||||
func (conn *ClientConn) Write(payload []byte) (int, error) {
|
||||
n := uint(len(payload))
|
||||
var chunkLen uint
|
||||
for i := uint(0); i < n; i += conn.mss {
|
||||
if i+conn.mss > n {
|
||||
chunkLen = n % conn.mss
|
||||
} else {
|
||||
chunkLen = conn.mss
|
||||
}
|
||||
chunk := payload[i : i+chunkLen]
|
||||
conn.seqCond.L.Lock()
|
||||
conn.waitForClientAcks(uint32(chunkLen))
|
||||
conn.tcpSend(chunk, TCPFlagAck)
|
||||
conn.seqCond.L.Unlock()
|
||||
}
|
||||
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
// check peer's receive window and wait for it to open if necessary
|
||||
func (conn *ClientConn) waitForClientAcks(i uint32) {
|
||||
for conn.sndUna+uint32(i)-conn.rcvAckNum > uint32(conn.rcvAdv)<<conn.rcvWndScale {
|
||||
if conn.rcvAdv == 0 {
|
||||
conn.log.Debug("window closed. punting.")
|
||||
return
|
||||
}
|
||||
conn.log.Debugf("waiting for client to ack byte %d", conn.sndUna)
|
||||
conn.seqCond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *ClientConn) Close() error {
|
||||
queuesMtx.Lock()
|
||||
delete(queues, conn.clientKey) // prevent writes on outQueue which is about to be closed
|
||||
queuesMtx.Unlock()
|
||||
|
||||
close(conn.rxq)
|
||||
|
||||
conn.log.Debugf("closing state = %d", conn.tState)
|
||||
if conn.tState == tcpsEstablished || conn.tState == tcpsSynReceived {
|
||||
conn.seqCond.L.Lock()
|
||||
conn.waitForClientAcks(0)
|
||||
conn.tcpSend(nil, TCPFlagFin)
|
||||
conn.seqCond.L.Unlock()
|
||||
conn.log.Debug("FIN sent")
|
||||
conn.tState = tcpsFinWait1
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *ClientConn) LocalAddr() net.Addr {
|
||||
return conn.dstAddr
|
||||
}
|
||||
|
||||
func (conn *ClientConn) RemoteAddr() net.Addr {
|
||||
return conn.srcAddr
|
||||
}
|
||||
|
||||
func (conn *ClientConn) SetDeadline(t time.Time) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (conn *ClientConn) SetReadDeadline(t time.Time) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (conn *ClientConn) SetWriteDeadline(t time.Time) error {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (b TCP) HasFlags(flags uint8) bool {
|
||||
return b.Flags()&flags == flags
|
||||
}
|
||||
|
||||
const (
|
||||
// Maximum space available for options.
|
||||
maxOptionSize = 40
|
||||
)
|
||||
|
||||
func makeSynOptions(opts TCPSynOptions) []byte {
|
||||
// Emulate linux option order. This is as follows:
|
||||
//
|
||||
// if md5: NOP NOP MD5SIG 18 md5sig(16)
|
||||
// if mss: MSS 4 mss(2)
|
||||
// if ts and sack_advertise:
|
||||
// SACK 2 TIMESTAMP 2 timestamp(8)
|
||||
// elif ts: NOP NOP TIMESTAMP 10 timestamp(8)
|
||||
// elif sack: NOP NOP SACK 2
|
||||
// if wscale: NOP WINDOW 3 ws(1)
|
||||
// if sack_blocks: NOP NOP SACK ((2 + (#blocks * 8))
|
||||
// [for each block] start_seq(4) end_seq(4)
|
||||
// if fastopen_cookie:
|
||||
// if exp: EXP (4 + len(cookie)) FASTOPEN_MAGIC(2)
|
||||
// else: FASTOPEN (2 + len(cookie))
|
||||
// cookie(variable) [padding to four bytes]
|
||||
//
|
||||
options := make([]byte, maxOptionSize)
|
||||
|
||||
// Always encode the mss.
|
||||
offset := EncodeMSSOption(uint32(opts.MSS), options)
|
||||
|
||||
// Special ordering is required here. If both TS and SACK are enabled,
|
||||
// then the SACK option precedes TS, with no padding. If they are
|
||||
// enabled individually, then we see padding before the option.
|
||||
if opts.TS && opts.SACKPermitted {
|
||||
offset += EncodeSACKPermittedOption(options[offset:])
|
||||
offset += EncodeTSOption(opts.TSVal, opts.TSEcr, options[offset:])
|
||||
} else if opts.TS {
|
||||
offset += EncodeNOP(options[offset:])
|
||||
offset += EncodeNOP(options[offset:])
|
||||
offset += EncodeTSOption(opts.TSVal, opts.TSEcr, options[offset:])
|
||||
} else if opts.SACKPermitted {
|
||||
offset += EncodeNOP(options[offset:])
|
||||
offset += EncodeNOP(options[offset:])
|
||||
offset += EncodeSACKPermittedOption(options[offset:])
|
||||
}
|
||||
|
||||
// Initialize the WS option.
|
||||
if opts.WS >= 0 {
|
||||
offset += EncodeNOP(options[offset:])
|
||||
offset += EncodeWSOption(opts.WS, options[offset:])
|
||||
}
|
||||
|
||||
// Padding to the end; note that this never apply unless we add a
|
||||
// fastopen option, we always expect the offset to remain the same.
|
||||
if delta := AddTCPOptionPadding(options, offset); delta != 0 {
|
||||
panic("unexpected option encoding")
|
||||
}
|
||||
|
||||
return options[:offset]
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
// lifted from https://github.com/google/netstack/blob/master/tcpip/header/tcp.go
|
||||
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/ip"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
srcPort = 0
|
||||
dstPort = 2
|
||||
seqNum = 4
|
||||
ackNum = 8
|
||||
dataOffset = 12
|
||||
tcpFlags = 13
|
||||
winSize = 14
|
||||
tcpChecksum = 16
|
||||
urgentPtr = 18
|
||||
)
|
||||
|
||||
const (
|
||||
// MaxWndScale is maximum allowed window scaling, as described in
|
||||
// RFC 1323, section 2.3, page 11.
|
||||
MaxWndScale = 14
|
||||
|
||||
// TCPMaxSACKBlocks is the maximum number of SACK blocks that can
|
||||
// be encoded in a TCP option field.
|
||||
TCPMaxSACKBlocks = 4
|
||||
)
|
||||
|
||||
type TCP []byte
|
||||
|
||||
const (
|
||||
// TCPMinimumSize is the minimum size of a valid TCP packet.
|
||||
TCPMinimumSize = 20
|
||||
|
||||
// TCPProtocolNumber is TCP's transport protocol number.
|
||||
TCPProtocolNumber int = 6
|
||||
)
|
||||
|
||||
// Flags that may be set in a TCP segment.
|
||||
const (
|
||||
TCPFlagFin = 1 << iota
|
||||
TCPFlagSyn
|
||||
TCPFlagRst
|
||||
TCPFlagPsh
|
||||
TCPFlagAck
|
||||
TCPFlagUrg
|
||||
)
|
||||
|
||||
// Options that may be present in a TCP segment.
|
||||
const (
|
||||
TCPOptionEOL = 0
|
||||
TCPOptionNOP = 1
|
||||
TCPOptionMSS = 2
|
||||
TCPOptionWS = 3
|
||||
TCPOptionTS = 8
|
||||
TCPOptionSACKPermitted = 4
|
||||
TCPOptionSACK = 5
|
||||
)
|
||||
|
||||
// TCPFields contains the fields of a TCP packet. It is used to describe the
|
||||
// fields of a packet that needs to be encoded.
|
||||
type TCPFields struct {
|
||||
// SrcPort is the "source port" field of a TCP packet.
|
||||
SrcPort uint16
|
||||
|
||||
// DstPort is the "destination port" field of a TCP packet.
|
||||
DstPort uint16
|
||||
|
||||
// SeqNum is the "sequence number" field of a TCP packet.
|
||||
SeqNum uint32
|
||||
|
||||
// AckNum is the "acknowledgement number" field of a TCP packet.
|
||||
AckNum uint32
|
||||
|
||||
// DataOffset is the "data offset" field of a TCP packet.
|
||||
DataOffset uint8
|
||||
|
||||
// Flags is the "flags" field of a TCP packet.
|
||||
Flags uint8
|
||||
|
||||
// WindowSize is the "window size" field of a TCP packet.
|
||||
WindowSize uint16
|
||||
|
||||
// Checksum is the "checksum" field of a TCP packet.
|
||||
Checksum uint16
|
||||
|
||||
// UrgentPointer is the "urgent pointer" field of a TCP packet.
|
||||
UrgentPointer uint16
|
||||
}
|
||||
|
||||
type TCPSynOptions struct {
|
||||
// MSS is the maximum segment size provided by the peer in the SYN.
|
||||
MSS uint16
|
||||
|
||||
// WS is the window scale option provided by the peer in the SYN.
|
||||
//
|
||||
// Set to -1 if no window scale option was provided.
|
||||
WS int
|
||||
|
||||
// TS is true if the timestamp option was provided in the syn/syn-ack.
|
||||
TS bool
|
||||
|
||||
// TSVal is the value of the TSVal field in the timestamp option.
|
||||
TSVal uint32
|
||||
|
||||
// TSEcr is the value of the TSEcr field in the timestamp option.
|
||||
TSEcr uint32
|
||||
|
||||
// SACKPermitted is true if the SACK option was provided in the SYN/SYN-ACK.
|
||||
SACKPermitted bool
|
||||
}
|
||||
|
||||
// SourcePort returns the "source port" field of the tcp header.
|
||||
func (b TCP) SourcePort() uint16 {
|
||||
return binary.BigEndian.Uint16(b[srcPort:])
|
||||
}
|
||||
|
||||
// DestinationPort returns the "destination port" field of the tcp header.
|
||||
func (b TCP) DestinationPort() uint16 {
|
||||
return binary.BigEndian.Uint16(b[dstPort:])
|
||||
}
|
||||
|
||||
// SequenceNumber returns the "sequence number" field of the tcp header.
|
||||
func (b TCP) SequenceNumber() uint32 {
|
||||
return binary.BigEndian.Uint32(b[seqNum:])
|
||||
}
|
||||
|
||||
// AckNumber returns the "ack number" field of the tcp header.
|
||||
func (b TCP) AckNumber() uint32 {
|
||||
return binary.BigEndian.Uint32(b[ackNum:])
|
||||
}
|
||||
|
||||
// DataOffset returns the "data offset" field of the tcp header.
|
||||
func (b TCP) DataOffset() uint8 {
|
||||
return (b[dataOffset] >> 4) * 4
|
||||
}
|
||||
|
||||
// Payload returns the data in the tcp packet.
|
||||
func (b TCP) Payload() []byte {
|
||||
return b[b.DataOffset():]
|
||||
}
|
||||
|
||||
// Flags returns the flags field of the tcp header.
|
||||
func (b TCP) Flags() uint8 {
|
||||
return b[tcpFlags]
|
||||
}
|
||||
|
||||
// WindowSize returns the "window size" field of the tcp header.
|
||||
func (b TCP) WindowSize() uint16 {
|
||||
return binary.BigEndian.Uint16(b[winSize:])
|
||||
}
|
||||
|
||||
// Checksum returns the "checksum" field of the tcp header.
|
||||
func (b TCP) Checksum() uint16 {
|
||||
return binary.BigEndian.Uint16(b[tcpChecksum:])
|
||||
}
|
||||
|
||||
// SetSourcePort sets the "source port" field of the tcp header.
|
||||
func (b TCP) SetSourcePort(port uint16) {
|
||||
binary.BigEndian.PutUint16(b[srcPort:], port)
|
||||
}
|
||||
|
||||
// SetDestinationPort sets the "destination port" field of the tcp header.
|
||||
func (b TCP) SetDestinationPort(port uint16) {
|
||||
binary.BigEndian.PutUint16(b[dstPort:], port)
|
||||
}
|
||||
|
||||
// SetChecksum sets the checksum field of the tcp header.
|
||||
func (b TCP) SetChecksum(checksum uint16) {
|
||||
binary.BigEndian.PutUint16(b[tcpChecksum:], checksum)
|
||||
}
|
||||
|
||||
// CalculateChecksum calculates the checksum of the tcp segment given
|
||||
// the totalLen and partialChecksum(descriptions below)
|
||||
// totalLen is the total length of the segment
|
||||
// partialChecksum is the checksum of the network-layer pseudo-header
|
||||
// (excluding the total length) and the checksum of the segment data.
|
||||
func (b TCP) CalculateChecksum(partialChecksum uint16, totalLen uint16) uint16 {
|
||||
// Add the length portion of the checksum to the pseudo-checksum.
|
||||
tmp := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(tmp, totalLen)
|
||||
checksum := ip.Checksum(tmp, partialChecksum)
|
||||
|
||||
// Calculate the rest of the checksum.
|
||||
return ip.Checksum(b[:b.DataOffset()], checksum)
|
||||
}
|
||||
|
||||
func (b TCP) encodeSubset(seq, ack uint32, flags uint8, rcvwnd uint16) {
|
||||
binary.BigEndian.PutUint32(b[seqNum:], seq)
|
||||
binary.BigEndian.PutUint32(b[ackNum:], ack)
|
||||
b[tcpFlags] = flags
|
||||
binary.BigEndian.PutUint16(b[winSize:], rcvwnd)
|
||||
}
|
||||
|
||||
// Encode encodes all the fields of the tcp header.
|
||||
func (b TCP) Encode(t *TCPFields) {
|
||||
b.encodeSubset(t.SeqNum, t.AckNum, t.Flags, t.WindowSize)
|
||||
binary.BigEndian.PutUint16(b[srcPort:], t.SrcPort)
|
||||
binary.BigEndian.PutUint16(b[dstPort:], t.DstPort)
|
||||
b[dataOffset] = (t.DataOffset / 4) << 4
|
||||
binary.BigEndian.PutUint16(b[tcpChecksum:], t.Checksum)
|
||||
binary.BigEndian.PutUint16(b[urgentPtr:], t.UrgentPointer)
|
||||
}
|
||||
|
||||
// EncodePartial updates a subset of the fields of the tcp header. It is useful
|
||||
// in cases when similar segments are produced.
|
||||
func (b TCP) EncodePartial(partialChecksum, length uint16, seqnum, acknum uint32, flags byte, rcvwnd uint16) {
|
||||
// Add the total length and "flags" field contributions to the checksum.
|
||||
// We don't use the flags field directly from the header because it's a
|
||||
// one-byte field with an odd offset, so it would be accounted for
|
||||
// incorrectly by the Checksum routine.
|
||||
tmp := make([]byte, 4)
|
||||
binary.BigEndian.PutUint16(tmp, length)
|
||||
binary.BigEndian.PutUint16(tmp[2:], uint16(flags))
|
||||
checksum := ip.Checksum(tmp, partialChecksum)
|
||||
|
||||
// Encode the passed-in fields.
|
||||
b.encodeSubset(seqnum, acknum, flags, rcvwnd)
|
||||
|
||||
// Add the contributions of the passed-in fields to the checksum.
|
||||
checksum = ip.Checksum(b[seqNum:seqNum+8], checksum)
|
||||
checksum = ip.Checksum(b[winSize:winSize+2], checksum)
|
||||
|
||||
// Encode the checksum.
|
||||
b.SetChecksum(^checksum)
|
||||
}
|
||||
|
||||
// ParseSynOptions parses the options received in a SYN segment and returns the
|
||||
// relevant ones. opts should point to the option part of the TCP Header.
|
||||
func ParseSynOptions(opts []byte, isAck bool) TCPSynOptions {
|
||||
limit := len(opts)
|
||||
|
||||
synOpts := TCPSynOptions{
|
||||
// Per RFC 1122, page 85: "If an MSS option is not received at
|
||||
// connection setup, TCP MUST assume a default send MSS of 536."
|
||||
MSS: 536,
|
||||
// If no window scale option is specified, WS in options is
|
||||
// returned as -1; this is because the absence of the option
|
||||
// indicates that the we cannot use window scaling on the
|
||||
// receive end either.
|
||||
WS: -1,
|
||||
}
|
||||
|
||||
for i := 0; i < limit; {
|
||||
switch opts[i] {
|
||||
case TCPOptionEOL:
|
||||
i = limit
|
||||
case TCPOptionNOP:
|
||||
i++
|
||||
case TCPOptionMSS:
|
||||
if i+4 > limit || opts[i+1] != 4 {
|
||||
return synOpts
|
||||
}
|
||||
mss := uint16(opts[i+2])<<8 | uint16(opts[i+3])
|
||||
if mss == 0 {
|
||||
return synOpts
|
||||
}
|
||||
synOpts.MSS = mss
|
||||
i += 4
|
||||
|
||||
case TCPOptionWS:
|
||||
if i+3 > limit || opts[i+1] != 3 {
|
||||
return synOpts
|
||||
}
|
||||
ws := int(opts[i+2])
|
||||
if ws > MaxWndScale {
|
||||
ws = MaxWndScale
|
||||
}
|
||||
synOpts.WS = ws
|
||||
i += 3
|
||||
|
||||
case TCPOptionTS:
|
||||
if i+10 > limit || opts[i+1] != 10 {
|
||||
return synOpts
|
||||
}
|
||||
synOpts.TSVal = binary.BigEndian.Uint32(opts[i+2:])
|
||||
if isAck {
|
||||
// If the segment is a SYN-ACK then store the Timestamp Echo Reply
|
||||
// in the segment.
|
||||
synOpts.TSEcr = binary.BigEndian.Uint32(opts[i+6:])
|
||||
}
|
||||
synOpts.TS = true
|
||||
i += 10
|
||||
case TCPOptionSACKPermitted:
|
||||
if i+2 > limit || opts[i+1] != 2 {
|
||||
return synOpts
|
||||
}
|
||||
synOpts.SACKPermitted = true
|
||||
i += 2
|
||||
|
||||
default:
|
||||
// We don't recognize this option, just skip over it.
|
||||
if i+2 > limit {
|
||||
return synOpts
|
||||
}
|
||||
l := int(opts[i+1])
|
||||
// If the length is incorrect or if l+i overflows the
|
||||
// total options length then return false.
|
||||
if l < 2 || i+l > limit {
|
||||
return synOpts
|
||||
}
|
||||
i += l
|
||||
}
|
||||
}
|
||||
|
||||
return synOpts
|
||||
}
|
||||
|
||||
// EncodeMSSOption encodes the MSS TCP option with the provided MSS values in
|
||||
// the supplied buffer. If the provided buffer is not large enough then it just
|
||||
// returns without encoding anything. It returns the number of bytes written to
|
||||
// the provided buffer.
|
||||
func EncodeMSSOption(mss uint32, b []byte) int {
|
||||
// mssOptionSize is the number of bytes in a valid MSS option.
|
||||
const mssOptionSize = 4
|
||||
|
||||
if len(b) < mssOptionSize {
|
||||
return 0
|
||||
}
|
||||
b[0], b[1], b[2], b[3] = TCPOptionMSS, mssOptionSize, byte(mss>>8), byte(mss)
|
||||
return mssOptionSize
|
||||
}
|
||||
|
||||
// EncodeWSOption encodes the WS TCP option with the WS value in the
|
||||
// provided buffer. If the provided buffer is not large enough then it just
|
||||
// returns without encoding anything. It returns the number of bytes written to
|
||||
// the provided buffer.
|
||||
func EncodeWSOption(ws int, b []byte) int {
|
||||
if len(b) < 3 {
|
||||
return 0
|
||||
}
|
||||
b[0], b[1], b[2] = TCPOptionWS, 3, uint8(ws)
|
||||
return int(b[1])
|
||||
}
|
||||
|
||||
// EncodeTSOption encodes the provided tsVal and tsEcr values as a TCP timestamp
|
||||
// option into the provided buffer. If the buffer is smaller than expected it
|
||||
// just returns without encoding anything. It returns the number of bytes
|
||||
// written to the provided buffer.
|
||||
func EncodeTSOption(tsVal, tsEcr uint32, b []byte) int {
|
||||
if len(b) < 10 {
|
||||
return 0
|
||||
}
|
||||
b[0], b[1] = TCPOptionTS, 10
|
||||
binary.BigEndian.PutUint32(b[2:], tsVal)
|
||||
binary.BigEndian.PutUint32(b[6:], tsEcr)
|
||||
return int(b[1])
|
||||
}
|
||||
|
||||
// EncodeSACKPermittedOption encodes a SACKPermitted option into the provided
|
||||
// buffer. If the buffer is smaller than required it just returns without
|
||||
// encoding anything. It returns the number of bytes written to the provided
|
||||
// buffer.
|
||||
func EncodeSACKPermittedOption(b []byte) int {
|
||||
if len(b) < 2 {
|
||||
return 0
|
||||
}
|
||||
|
||||
b[0], b[1] = TCPOptionSACKPermitted, 2
|
||||
return int(b[1])
|
||||
}
|
||||
|
||||
// EncodeNOP adds an explicit NOP to the option list.
|
||||
func EncodeNOP(b []byte) int {
|
||||
if len(b) == 0 {
|
||||
return 0
|
||||
}
|
||||
b[0] = TCPOptionNOP
|
||||
return 1
|
||||
}
|
||||
|
||||
// AddTCPOptionPadding adds the required number of TCPOptionNOP to quad align
|
||||
// the option buffer. It adds padding bytes after the offset specified and
|
||||
// returns the number of padding bytes added. The passed in options slice
|
||||
// must have space for the padding bytes.
|
||||
func AddTCPOptionPadding(options []byte, offset int) int {
|
||||
paddingToAdd := -offset & 3
|
||||
// Now add any padding bytes that might be required to quad align the
|
||||
// options.
|
||||
for i := offset; i < offset+paddingToAdd; i++ {
|
||||
options[i] = TCPOptionNOP
|
||||
}
|
||||
return paddingToAdd
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/tunnel"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var log = logrus.StandardLogger()
|
||||
|
||||
// packet queues (outbound) keyed by client address
|
||||
type tcpQItem struct {
|
||||
segment TCP // the actual segment
|
||||
release func() // a closure that returns the underlying packet to the rx packet pool when called.
|
||||
}
|
||||
|
||||
var queues = make(map[string]chan *tcpQItem)
|
||||
var queuesMtx = sync.Mutex{}
|
||||
|
||||
// handled service names keyed by intercept address
|
||||
var services = make(map[string]string)
|
||||
var servicesMtx = sync.Mutex{}
|
||||
|
||||
func key(ip net.IP, port int) string {
|
||||
return fmt.Sprintf("%s:%d", ip.String(), port)
|
||||
}
|
||||
|
||||
func RegisterService(service edge.Service, interceptIP net.IP) {
|
||||
key := key(interceptIP, service.Dns.Port)
|
||||
servicesMtx.Lock()
|
||||
defer servicesMtx.Unlock()
|
||||
_, exists := services[key]
|
||||
if !exists {
|
||||
services[key] = service.Name
|
||||
}
|
||||
}
|
||||
|
||||
func UnregisterService(serviceName string) {
|
||||
var key string
|
||||
servicesMtx.Lock()
|
||||
defer servicesMtx.Unlock()
|
||||
|
||||
for addr, svcName := range services {
|
||||
if svcName == serviceName {
|
||||
key = addr
|
||||
break
|
||||
}
|
||||
}
|
||||
if key == "" {
|
||||
log.Warnf("service %s is not registered", serviceName)
|
||||
return
|
||||
}
|
||||
|
||||
delete(services, key)
|
||||
}
|
||||
|
||||
func Enqueue(context ziti.Context, srcIP, dstIP net.IP, pdu []byte, dev io.ReadWriter, tunMTU uint, release func()) bool {
|
||||
segment := TCP(pdu)
|
||||
svcKey := key(dstIP, int(segment.DestinationPort()))
|
||||
service := services[svcKey]
|
||||
if service == "" {
|
||||
log.Debugf("no service registered for %s", svcKey)
|
||||
return false
|
||||
}
|
||||
|
||||
clientKey := key(srcIP, int(segment.SourcePort()))
|
||||
queuesMtx.Lock()
|
||||
defer queuesMtx.Unlock()
|
||||
queue := queues[clientKey]
|
||||
if queue == nil {
|
||||
if segment.HasFlags(TCPFlagSyn) {
|
||||
queue = make(chan *tcpQItem, 64)
|
||||
queues[clientKey] = queue
|
||||
clientConn, err := NewClientConn(clientKey, svcKey, queue, dev, tunMTU)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create client connection for %s, %s: %v", clientKey, svcKey, err)
|
||||
release()
|
||||
return true // this packet is effectively handled here, even though we're dropping it
|
||||
}
|
||||
go tunnel.Run(context, service, clientConn)
|
||||
} else {
|
||||
log.Debugf("packet from %s lacks TCP syn", clientKey)
|
||||
release()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var x tcpQItem
|
||||
x.release = release
|
||||
x.segment = segment
|
||||
queue <- &x
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/ip"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
maxPacketSize = 65535
|
||||
maxPayloadSize = 65507
|
||||
|
||||
ProtocolNumber int = 17
|
||||
)
|
||||
|
||||
type ClientConn struct {
|
||||
net.Conn
|
||||
|
||||
svcKey string
|
||||
dstAddr *net.UDPAddr
|
||||
txq chan []byte // queue of layer 3 packets for peer
|
||||
dev io.ReadWriter
|
||||
|
||||
log *logrus.Entry
|
||||
}
|
||||
|
||||
func NewClientConn(interceptAddr string, dev io.ReadWriter) (*ClientConn, error) {
|
||||
dstAddr, err := net.ResolveUDPAddr("udp", interceptAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
txq := make(chan []byte, 16)
|
||||
for i := 0; i < cap(txq); i++ {
|
||||
txq <- make([]byte, maxPacketSize)
|
||||
}
|
||||
return &ClientConn{
|
||||
svcKey: interceptAddr,
|
||||
dstAddr: dstAddr,
|
||||
txq: txq,
|
||||
dev: dev,
|
||||
log: log.WithFields(logrus.Fields{"dst": dstAddr.String()}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Write to the local client.
|
||||
func (conn *ClientConn) udpSend(payload []byte, addr *net.UDPAddr) {
|
||||
dataLen := len(payload)
|
||||
log.Infof("Getting next packet from pool")
|
||||
packet := <-conn.txq
|
||||
log.Infof("Received packet from pool")
|
||||
defer func() {
|
||||
conn.txq <- packet
|
||||
}()
|
||||
packet = packet[:ip.IPv4MinimumSize+headerSize+dataLen]
|
||||
ipHeader := ip.IPv4(packet[0:ip.IPv4MinimumSize])
|
||||
ipHeader.Encode(&ip.IPv4Fields{
|
||||
IHL: ip.IPv4MinimumSize,
|
||||
TotalLength: uint16(len(packet)),
|
||||
Protocol: uint8(ProtocolNumber),
|
||||
SrcAddr: conn.dstAddr.IP.To4(),
|
||||
DstAddr: addr.IP.To4(),
|
||||
TTL: 64,
|
||||
})
|
||||
ipHeader.SetChecksum(^ipHeader.CalculateChecksum())
|
||||
|
||||
udpHeader := UDP(packet[ip.IPv4MinimumSize : ip.IPv4MinimumSize+headerSize])
|
||||
udpHeader.Encode(&udpFields{
|
||||
SrcPort: uint16(conn.dstAddr.Port),
|
||||
DstPort: uint16(addr.Port),
|
||||
Length: headerSize + uint16(dataLen),
|
||||
Checksum: 0,
|
||||
})
|
||||
|
||||
sum := ip.PseudoHeaderChecksum(ProtocolNumber, conn.dstAddr.IP.To4(), addr.IP.To4())
|
||||
sum = ip.ChecksumCombine(sum, udpHeader.Length()) // need to UDP length field to IP pseudo header
|
||||
sum = ip.Checksum(payload, sum)
|
||||
sum = ip.Checksum(udpHeader, sum)
|
||||
udpHeader.SetChecksum(^sum)
|
||||
|
||||
copy(packet[ip.IPv4MinimumSize+headerSize:], payload)
|
||||
conn.log.Infof("sending len %d", dataLen)
|
||||
nOut, err := conn.dev.Write(packet)
|
||||
if nOut != len(packet) {
|
||||
conn.log.Errorf("short write %d of %d bytes %v", nOut, len(packet), err)
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *ClientConn) WriteTo(payload []byte, addr net.Addr) (int, error) {
|
||||
n := uint(len(payload))
|
||||
log.Infof("Received %v bytes from Ziti", n)
|
||||
var chunkLen uint
|
||||
for i := uint(0); i < n; i += maxPayloadSize {
|
||||
if i+maxPayloadSize > n {
|
||||
chunkLen = n % maxPayloadSize
|
||||
} else {
|
||||
chunkLen = maxPayloadSize
|
||||
}
|
||||
chunk := payload[i : i+chunkLen]
|
||||
log.Infof("Sending bytes [%v, %v] back", i, i+chunkLen)
|
||||
conn.udpSend(chunk, addr.(*net.UDPAddr))
|
||||
}
|
||||
|
||||
return int(n), nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
// lifted from https://github.com/google/netstack/blob/master/tcpip/header/tcp.go
|
||||
|
||||
// Copyright 2018 Google Inc.
|
||||
//
|
||||
// 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.
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/ip"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
srcPortOffset = 0
|
||||
dstPortOffset = 2
|
||||
lengthOffset = 4
|
||||
checksumOffset = 6
|
||||
headerSize = 8
|
||||
)
|
||||
|
||||
type UDP []byte
|
||||
|
||||
// udpFields contains the fields of a UDP packet. It is used to describe the
|
||||
// fields of a packet that needs to be encoded.
|
||||
type udpFields struct {
|
||||
// SrcPort is the "source port" field of a UDP packet.
|
||||
SrcPort uint16
|
||||
|
||||
// DstPort is the "destination port" field of a UDP packet.
|
||||
DstPort uint16
|
||||
|
||||
// Length is the "length" field of a UDP packet.
|
||||
Length uint16
|
||||
|
||||
// Checksum is the "checksum" field of a UDP packet.
|
||||
Checksum uint16
|
||||
}
|
||||
|
||||
// SourcePort returns the "source port" field of the udp header.
|
||||
func (b UDP) SourcePort() uint16 {
|
||||
return binary.BigEndian.Uint16(b[srcPortOffset:])
|
||||
}
|
||||
|
||||
// DestinationPort returns the "destination port" field of the udp header.
|
||||
func (b UDP) DestinationPort() uint16 {
|
||||
return binary.BigEndian.Uint16(b[dstPortOffset:])
|
||||
}
|
||||
|
||||
// SequenceNumber returns the "sequence number" field of the udp header.
|
||||
func (b UDP) Length() uint16 {
|
||||
return binary.BigEndian.Uint16(b[lengthOffset:])
|
||||
}
|
||||
|
||||
// Payload returns the data in the tcp packet.
|
||||
func (b UDP) Payload() []byte {
|
||||
return b[headerSize:]
|
||||
}
|
||||
|
||||
// Checksum returns the "checksum" field of the tcp header.
|
||||
func (b UDP) Checksum() uint16 {
|
||||
return binary.BigEndian.Uint16(b[checksumOffset:])
|
||||
}
|
||||
|
||||
// SetSourcePort sets the "source port" field of the tcp header.
|
||||
func (b UDP) SetSourcePort(port uint16) {
|
||||
binary.BigEndian.PutUint16(b[srcPortOffset:], port)
|
||||
}
|
||||
|
||||
// SetDestinationPort sets the "destination port" field of the tcp header.
|
||||
func (b UDP) SetDestinationPort(port uint16) {
|
||||
binary.BigEndian.PutUint16(b[dstPortOffset:], port)
|
||||
}
|
||||
|
||||
// SetChecksum sets the checksum field of the tcp header.
|
||||
func (b UDP) SetChecksum(checksum uint16) {
|
||||
binary.BigEndian.PutUint16(b[checksumOffset:], checksum)
|
||||
}
|
||||
|
||||
func (b UDP) CalculateChecksum() uint16 {
|
||||
return ip.Checksum(b, 0)
|
||||
}
|
||||
|
||||
// Encode encodes all the fields of the udp header.
|
||||
func (b UDP) Encode(fields *udpFields) {
|
||||
binary.BigEndian.PutUint16(b[srcPortOffset:], fields.SrcPort)
|
||||
binary.BigEndian.PutUint16(b[dstPortOffset:], fields.DstPort)
|
||||
binary.BigEndian.PutUint16(b[lengthOffset:], fields.Length)
|
||||
binary.BigEndian.PutUint16(b[checksumOffset:], fields.Checksum)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package udp
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/udp_vconn"
|
||||
"fmt"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var log = logrus.StandardLogger()
|
||||
|
||||
// packet queues (outbound) keyed by client address
|
||||
type udpQItem struct {
|
||||
segment UDP // the actual segment
|
||||
srcIP, dstIP net.IP
|
||||
tunManager *TunUDPManager
|
||||
release func() // a closure that returns the underlying packet to the rx packet pool when called.
|
||||
}
|
||||
|
||||
func (item *udpQItem) GetPayload() []byte {
|
||||
return item.segment.Payload()
|
||||
}
|
||||
|
||||
func (item *udpQItem) Release() {
|
||||
item.release()
|
||||
}
|
||||
|
||||
func NewManager(dev io.ReadWriter) *TunUDPManager {
|
||||
return &TunUDPManager{
|
||||
dev: dev,
|
||||
services: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
type TunUDPManager struct {
|
||||
dev io.ReadWriter
|
||||
services map[string]string
|
||||
servicesMtx sync.Mutex
|
||||
}
|
||||
|
||||
func (manager *TunUDPManager) RegisterService(service edge.Service, interceptIP net.IP) {
|
||||
addr := &net.UDPAddr{IP: interceptIP, Port: service.Dns.Port}
|
||||
key := addr.String()
|
||||
log.Infof("registered udp service %s", key)
|
||||
manager.servicesMtx.Lock()
|
||||
defer manager.servicesMtx.Unlock()
|
||||
_, exists := manager.services[key]
|
||||
if !exists {
|
||||
manager.services[key] = service.Name
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *TunUDPManager) UnregisterService(service string) {
|
||||
var key string
|
||||
manager.servicesMtx.Lock()
|
||||
defer manager.servicesMtx.Unlock()
|
||||
|
||||
for addr, svcName := range manager.services {
|
||||
if svcName == service {
|
||||
key = addr
|
||||
break
|
||||
}
|
||||
}
|
||||
if key == "" {
|
||||
log.Warnf("service %s is not registered", service)
|
||||
return
|
||||
}
|
||||
|
||||
delete(manager.services, key)
|
||||
}
|
||||
|
||||
func (manager *TunUDPManager) CreateEvent(context ziti.Context, srcIP, dstIP net.IP, pdu []byte, dev io.ReadWriter, release func()) udp_vconn.Event {
|
||||
udpQItem := &udpQItem{
|
||||
segment: UDP(pdu),
|
||||
srcIP: srcIP,
|
||||
dstIP: dstIP,
|
||||
tunManager: manager,
|
||||
release: release,
|
||||
}
|
||||
return udpQItem
|
||||
}
|
||||
|
||||
func (item *udpQItem) Handle(proxyManager udp_vconn.Manager) error {
|
||||
tunManager := item.tunManager
|
||||
segment := item.segment
|
||||
|
||||
srcAddr := &net.UDPAddr{IP: item.srcIP, Port: int(segment.SourcePort())}
|
||||
|
||||
writeQueue := proxyManager.GetWriteQueue(srcAddr)
|
||||
if writeQueue == nil {
|
||||
dstAddr := &net.UDPAddr{IP: item.dstIP, Port: int(segment.DestinationPort())}
|
||||
svcKey := dstAddr.String()
|
||||
tunManager.servicesMtx.Lock()
|
||||
defer tunManager.servicesMtx.Unlock()
|
||||
service, found := tunManager.services[svcKey]
|
||||
if !found {
|
||||
return fmt.Errorf("no service found for intercept %v", svcKey)
|
||||
}
|
||||
|
||||
clientConn, err := NewClientConn(svcKey, tunManager.dev)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeQueue, err = proxyManager.CreateWriteQueue(srcAddr, service, clientConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("received %v bytes for manager %v -> %v", len(segment.Payload()), writeQueue.LocalAddr(), writeQueue.Service())
|
||||
writeQueue.Accept(item)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-foundation/transport/udp"
|
||||
"github.com/netfoundry/ziti-edge/tunnel"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/dns"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/udp_vconn"
|
||||
"github.com/netfoundry/ziti-foundation/util/mempool"
|
||||
"fmt"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/pkg/errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Name string
|
||||
Port int
|
||||
Protocol intercept.Protocol
|
||||
}
|
||||
|
||||
type interceptor struct {
|
||||
interceptIP net.IP
|
||||
services map[string]*Service
|
||||
closeCh chan interface{}
|
||||
context ziti.Context
|
||||
}
|
||||
|
||||
func New(ip net.IP, services map[string]*Service) (intercept.Interceptor, error) {
|
||||
p := interceptor{
|
||||
interceptIP: ip,
|
||||
services: services,
|
||||
closeCh: make(chan interface{}),
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *interceptor) Start(context ziti.Context) {
|
||||
log := pfxlog.Logger()
|
||||
log.Info("starting proxy interceptor")
|
||||
|
||||
// just stash the context
|
||||
p.context = context
|
||||
}
|
||||
|
||||
func (p interceptor) Intercept(service edge.Service, resolver dns.Resolver) error {
|
||||
log := pfxlog.Logger().WithField("service", service.Name)
|
||||
|
||||
proxiedService, ok := p.services[service.Name]
|
||||
if !ok {
|
||||
log.Debug("service was not specified at initialization. not intercepting")
|
||||
return nil
|
||||
}
|
||||
|
||||
// pre-fetch network session todo move this to service poller?
|
||||
if ns, err := p.context.GetNetworkSession(service.Id); err != nil {
|
||||
return fmt.Errorf("failed to acquire network session: %v", err)
|
||||
} else {
|
||||
log.WithField("id", ns.Id).Debug("acquired network session")
|
||||
}
|
||||
|
||||
go p.runServiceListener(proxiedService)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *interceptor) runServiceListener(service *Service) {
|
||||
if service.Protocol == intercept.TCP {
|
||||
p.handleTCP(service)
|
||||
} else {
|
||||
p.handleUDP(service)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *interceptor) handleTCP(service *Service) {
|
||||
log := pfxlog.Logger().WithField("service", service.Name)
|
||||
|
||||
listenAddr := net.TCPAddr{IP: p.interceptIP, Port: service.Port}
|
||||
server, err := net.Listen("tcp4", listenAddr.String())
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
p.closeCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
log = log.WithField("addr", server.Addr().String())
|
||||
|
||||
log.Info("service is listening")
|
||||
defer log.Info("service stopped")
|
||||
defer func() {
|
||||
p.closeCh <- fmt.Sprintf("service listener %s exited", service.Name)
|
||||
}()
|
||||
|
||||
for {
|
||||
conn, err := server.Accept()
|
||||
if err != nil {
|
||||
log.WithError(err).Error("accept failed")
|
||||
p.closeCh <- err
|
||||
return
|
||||
}
|
||||
go tunnel.Run(p.context, service.Name, conn)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *interceptor) handleUDP(service *Service) {
|
||||
log := pfxlog.Logger().WithField("service", service.Name)
|
||||
|
||||
listenAddr := &net.UDPAddr{IP: p.interceptIP, Port: service.Port}
|
||||
udpPacketConn, err := net.ListenUDP("udp", listenAddr)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
p.closeCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
log = log.WithField("addr", udpPacketConn.LocalAddr().String())
|
||||
|
||||
log.Infof("service %v is listening", service.Name)
|
||||
reader := &udpReader{
|
||||
service: service.Name,
|
||||
conn: udpPacketConn,
|
||||
}
|
||||
vconnManager := udp_vconn.NewManager(p.context, udp_vconn.NewUnlimitedConnectionPolicy(), udp_vconn.NewDefaultExpirationPolicy())
|
||||
go reader.generateReadEvents(vconnManager)
|
||||
}
|
||||
|
||||
func (p *interceptor) Stop() {
|
||||
log := pfxlog.Logger()
|
||||
log.Info("stopping proxy interceptor")
|
||||
}
|
||||
|
||||
func (p *interceptor) StopIntercepting(serviceName string, removeRoute bool) error {
|
||||
return errors.New("StopIntercepting not implemented by proxy interceptor")
|
||||
}
|
||||
|
||||
type udpReader struct {
|
||||
service string
|
||||
conn *net.UDPConn
|
||||
}
|
||||
|
||||
func (reader *udpReader) generateReadEvents(manager udp_vconn.Manager) {
|
||||
log := pfxlog.Logger().WithField("service", reader.service)
|
||||
bufPool := mempool.NewPool(16, udp.MaxPacketSize)
|
||||
for {
|
||||
buf := bufPool.AcquireBuffer()
|
||||
n, srcAddr, err := reader.conn.ReadFromUDP(buf.Buf)
|
||||
if err != nil {
|
||||
log.WithError(err).Debug("failure while reading udp message")
|
||||
manager.QueueError(err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("read %v bytes from udp, queuing", len(buf.GetPayload()))
|
||||
buf.Buf = buf.Buf[:n]
|
||||
event := &udpReadEvent{
|
||||
reader: reader,
|
||||
buf: buf,
|
||||
srcAddr: srcAddr,
|
||||
}
|
||||
manager.QueueEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
type udpReadEvent struct {
|
||||
reader *udpReader
|
||||
buf *mempool.DefaultPooledBuffer
|
||||
srcAddr net.Addr
|
||||
}
|
||||
|
||||
func (event *udpReadEvent) Handle(manager udp_vconn.Manager) error {
|
||||
log := pfxlog.Logger()
|
||||
|
||||
writeQueue := manager.GetWriteQueue(event.srcAddr)
|
||||
|
||||
if writeQueue == nil {
|
||||
log.Infof("received connection for %v --> %v, which maps to intercepted service %v",
|
||||
event.srcAddr, event.reader.conn.LocalAddr(), event.reader.service)
|
||||
var err error
|
||||
writeQueue, err = manager.CreateWriteQueue(event.srcAddr, event.reader.service, event.reader.conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("received %v bytes for conn %v -> %v", len(event.buf.Buf), writeQueue.LocalAddr(), writeQueue.Service())
|
||||
writeQueue.Accept(event.buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package intercept
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/dns"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ServicePoller(context ziti.Context, interceptor Interceptor, resolver dns.Resolver, pollRate time.Duration) {
|
||||
interceptor.Start(context)
|
||||
knownServices := make(map[string]edge.Service)
|
||||
|
||||
if pollRate < time.Second {
|
||||
pollRate = 15 * time.Second
|
||||
}
|
||||
|
||||
for ; ; time.Sleep(pollRate) {
|
||||
edgeServices, err := context.GetServices()
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to get ziti services: %v", err)
|
||||
if err.Error() == "unauthorized" {
|
||||
if err := context.Authenticate(); err != nil {
|
||||
logrus.WithError(err).Error("could not re-authenticate, session lost")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// find new services
|
||||
addedServices := make(map[string]edge.Service)
|
||||
edgeServiceIds := make(map[string]struct{})
|
||||
for _, edgeSvc := range edgeServices {
|
||||
// get edge service IDs for efficiently finding removed services
|
||||
edgeServiceIds[edgeSvc.Id] = struct{}{}
|
||||
if _, ok := knownServices[edgeSvc.Id]; !ok {
|
||||
addedServices[edgeSvc.Id] = edgeSvc
|
||||
knownServices[edgeSvc.Id] = edgeSvc
|
||||
}
|
||||
}
|
||||
|
||||
// look for removed services
|
||||
removedServices := make(map[string]edge.Service)
|
||||
for id, knownSvc := range knownServices {
|
||||
if _, ok := edgeServiceIds[id]; !ok {
|
||||
removedServices[knownSvc.Id] = knownSvc
|
||||
delete(knownServices, id)
|
||||
}
|
||||
}
|
||||
|
||||
updateServices(context, interceptor, resolver, addedServices, removedServices, knownServices)
|
||||
}
|
||||
}
|
||||
|
||||
func updateServices(context ziti.Context, interceptor Interceptor, resolver dns.Resolver, added, removed, all map[string]edge.Service) {
|
||||
log := pfxlog.Logger()
|
||||
for _, svc := range added {
|
||||
log.Infof("starting tunnel for newly available service %s", svc.Name)
|
||||
err := interceptor.Intercept(svc, resolver)
|
||||
if err != nil {
|
||||
log.Errorf("failed to intercept service: %v", err)
|
||||
}
|
||||
|
||||
if dialAddr, ok := svc.Tags["tunneler.dial.addr"]; ok && svc.Hostable {
|
||||
log.Infof("Hosting newly available service %s", svc.Name)
|
||||
go host(context, svc, dialAddr)
|
||||
} else {
|
||||
if svc.Hostable {
|
||||
log.Infof("service %v is hostable but is missing a dial address. Add a 'tunneler.dial.addr' tag to the service to fix", svc.Name)
|
||||
} else {
|
||||
log.Debugf("service %v not hostable", svc.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// build map of all in-use address strings, so we know when a route needs to be removed from the tun
|
||||
allAddrs := make(map[string]*struct{}, len(all))
|
||||
for _, svc := range all {
|
||||
addr := svc.Dns.Hostname
|
||||
if _, ok := allAddrs[addr]; !ok {
|
||||
allAddrs[addr] = nil
|
||||
}
|
||||
}
|
||||
|
||||
for _, svc := range removed {
|
||||
log.Infof("stopping tunnel for unavailable service: %s", svc.Name)
|
||||
_, sharedAddress := allAddrs[svc.Dns.Hostname]
|
||||
err := interceptor.StopIntercepting(svc.Name, !sharedAddress)
|
||||
if err != nil {
|
||||
log.Errorf("failed to stop intercepting: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func host(context ziti.Context, svc edge.Service, addr string) {
|
||||
log := pfxlog.Logger()
|
||||
listener, err := context.Listen(svc.Name)
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("service", svc.Name).Errorf("error listening for service: %v", err)
|
||||
return
|
||||
}
|
||||
defer listener.Close()
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("service", svc.Name).Error("closing listener for service")
|
||||
return
|
||||
}
|
||||
addrParts := strings.SplitAfterN(addr, ":", 2)
|
||||
if len(addrParts) != 2 {
|
||||
log.WithError(err).
|
||||
WithField("service", svc.Name).
|
||||
WithField("dialAddr", addr).
|
||||
Error("unsupported external address")
|
||||
continue
|
||||
}
|
||||
externalConn, err := net.Dial(strings.TrimSuffix(addrParts[0], ":"), addrParts[1])
|
||||
if err != nil {
|
||||
log.WithError(err).
|
||||
WithField("service", svc.Name).
|
||||
WithField("dialAddr", addr).
|
||||
Error("dial failed")
|
||||
continue
|
||||
}
|
||||
log.WithField("service", svc.Name).
|
||||
WithField("dialAddr", addr).
|
||||
Error("hosting service, waiting for connections")
|
||||
pipe(svc, addr, conn, externalConn)
|
||||
}
|
||||
}
|
||||
|
||||
func pipe(svc edge.Service, addr string, zitiConn net.Conn, externalConn net.Conn) {
|
||||
log := pfxlog.Logger()
|
||||
closeReadC := make(chan struct{})
|
||||
closeWriteC := make(chan struct{})
|
||||
|
||||
copyAndClose := func(reader io.Reader, writer io.Writer, closeCh chan struct{}, context string) {
|
||||
_ = copy(reader, writer, context)
|
||||
close(closeCh)
|
||||
}
|
||||
|
||||
go copyAndClose(zitiConn, externalConn, closeWriteC, "->")
|
||||
go copyAndClose(externalConn, zitiConn, closeReadC, "<-")
|
||||
|
||||
go func() {
|
||||
defer externalConn.Close()
|
||||
defer zitiConn.Close()
|
||||
|
||||
<-closeReadC
|
||||
|
||||
log.WithField("service", svc.Name).WithField("dialAddr", addr).
|
||||
Info("communication complete, closing connections")
|
||||
}()
|
||||
}
|
||||
|
||||
func copy(reader io.Reader, writer io.Writer, context string) error {
|
||||
log := pfxlog.Logger().WithField("type", context)
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := reader.Read(buf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
log.Info("reached EOF on copy, returning")
|
||||
return nil
|
||||
}
|
||||
log.WithError(err).Error("error on copy read, returning")
|
||||
return err
|
||||
}
|
||||
log.WithError(err).Infof("read %v bytes", n)
|
||||
|
||||
writeBuf := buf[:n]
|
||||
n, err = writer.Write(writeBuf)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error on copy write, returning")
|
||||
return err
|
||||
}
|
||||
log.WithError(err).Infof("wrote %v bytes", n)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package tproxy
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/tunnel"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/dns"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/router"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/udp_vconn"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/coreos/go-iptables/iptables"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"github.com/netfoundry/ziti-foundation/transport/udp"
|
||||
"github.com/netfoundry/ziti-foundation/util/mempool"
|
||||
"golang.org/x/sys/unix"
|
||||
"net"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// https://github.com/torvalds/linux/blob/master/Documentation/networking/tproxy.txt
|
||||
|
||||
// Configure listening sockets with options that must be set before the socket is bound to an address (IP_TRANSPARENT).
|
||||
var listenConfig = net.ListenConfig{
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
var sockOptErr error
|
||||
controlErr := c.Control(func(sockFd uintptr) {
|
||||
// - https://www.kernel.org/doc/Documentation/networking/tproxy.txt
|
||||
if err := unix.SetsockoptInt(int(sockFd), unix.IPPROTO_IP, unix.IP_TRANSPARENT, 1); err != nil {
|
||||
sockOptErr = fmt.Errorf("error setting IP_TRANSPARENT socket option: %v", err)
|
||||
return
|
||||
}
|
||||
if err := unix.SetsockoptInt(int(sockFd), unix.SOL_SOCKET, unix.SO_REUSEADDR, 1); err != nil {
|
||||
sockOptErr = fmt.Errorf("error setting SO_REUSEADDR socket option: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := unix.SetsockoptInt(int(sockFd), syscall.SOL_IP, unix.IP_RECVORIGDSTADDR, 1); err != nil {
|
||||
sockOptErr = fmt.Errorf("error setting SO_REUSEADDR socket option: %v", err)
|
||||
return
|
||||
}
|
||||
})
|
||||
if controlErr != nil {
|
||||
return fmt.Errorf("error invoking listener socket control function: %v", controlErr)
|
||||
}
|
||||
return sockOptErr
|
||||
},
|
||||
}
|
||||
|
||||
type tProxyInterceptor struct {
|
||||
interceptLUT intercept.LUT
|
||||
ipt *iptables.IPTables
|
||||
tcpLn net.Listener
|
||||
udpLn *net.UDPConn
|
||||
}
|
||||
|
||||
const (
|
||||
mangleTable = "mangle"
|
||||
srcChain = "PREROUTING"
|
||||
dstChain = "NF-INTERCEPT"
|
||||
)
|
||||
|
||||
func contains(arr []string, str string) bool {
|
||||
for _, a := range arr {
|
||||
if a == str {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func New() (intercept.Interceptor, error) {
|
||||
log := pfxlog.Logger()
|
||||
tcpLn, err := listenConfig.Listen(context.Background(), "tcp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create TCP listener: %v", err)
|
||||
}
|
||||
log.Infof("tproxy listening on tcp:%s", tcpLn.Addr().String())
|
||||
|
||||
packetLn, err := listenConfig.ListenPacket(context.Background(), "udp", "127.0.0.1:")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create UDP listener: %v", err)
|
||||
}
|
||||
udpLn, ok := packetLn.(*net.UDPConn)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to create UDP listener. listener was not net.UDPConn")
|
||||
}
|
||||
log.Infof("tproxy listening on udp:%s, remoteAddr: %v", udpLn.LocalAddr(), udpLn.RemoteAddr())
|
||||
|
||||
ipt, err := iptables.New()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize iptables handle: %v", err)
|
||||
}
|
||||
|
||||
chains, err := ipt.ListChains(mangleTable)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list iptables chains: %v", err)
|
||||
}
|
||||
|
||||
if !contains(chains, dstChain) {
|
||||
err = ipt.NewChain(mangleTable, dstChain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create iptables chain: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
err = ipt.AppendUnique(mangleTable, srcChain, []string{"-j", dstChain}...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create '%s' --> '%s' link: %v", srcChain, dstChain, err)
|
||||
}
|
||||
|
||||
t := tProxyInterceptor{
|
||||
interceptLUT: intercept.NewLUT(),
|
||||
ipt: ipt,
|
||||
tcpLn: tcpLn,
|
||||
udpLn: udpLn,
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) Start(context ziti.Context) {
|
||||
t.accept(context)
|
||||
t.acceptUDP(context)
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) accept(context ziti.Context) {
|
||||
log := pfxlog.Logger()
|
||||
go func() {
|
||||
for {
|
||||
client, err := t.tcpLn.Accept()
|
||||
if err != nil {
|
||||
log.Errorf("error while accepting: %v", err)
|
||||
}
|
||||
if client == nil {
|
||||
log.Info("shutting down")
|
||||
return
|
||||
}
|
||||
log.Infof("received connection: %s --> %s", client.LocalAddr().String(), client.RemoteAddr().String())
|
||||
service, err := t.interceptLUT.GetByAddress(client.LocalAddr())
|
||||
if err != nil {
|
||||
log.Debugf("received connection for %s, which does not map to an intercepted service", client.LocalAddr().String())
|
||||
}
|
||||
go tunnel.Run(context, service.Name, client)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) acceptUDP(context ziti.Context) {
|
||||
vconnMgr := udp_vconn.NewManager(context, udp_vconn.NewUnlimitedConnectionPolicy(), udp_vconn.NewDefaultExpirationPolicy())
|
||||
go t.generateReadEvents(vconnMgr)
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) generateReadEvents(manager udp_vconn.Manager) {
|
||||
oobSize := 1600
|
||||
bufPool := mempool.NewPool(16, udp.MaxPacketSize+oobSize)
|
||||
|
||||
for {
|
||||
pooled := bufPool.AcquireBuffer()
|
||||
oob := pooled.Buf[udp.MaxPacketSize:]
|
||||
pooled.Buf = pooled.Buf[:udp.MaxPacketSize]
|
||||
n, oobn, _, srcAddr, err := t.udpLn.ReadMsgUDP(pooled.Buf, oob)
|
||||
if err != nil {
|
||||
manager.QueueError(err)
|
||||
return
|
||||
}
|
||||
pooled.Buf = pooled.Buf[:n]
|
||||
event := &udpReadEvent{
|
||||
interceptor: t,
|
||||
buf: pooled,
|
||||
oob: oob[:oobn],
|
||||
srcAddr: srcAddr,
|
||||
}
|
||||
manager.QueueEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
type udpReadEvent struct {
|
||||
interceptor *tProxyInterceptor
|
||||
buf *mempool.DefaultPooledBuffer
|
||||
oob []byte
|
||||
srcAddr net.Addr
|
||||
}
|
||||
|
||||
func (event *udpReadEvent) Handle(manager udp_vconn.Manager) error {
|
||||
writeQueue := manager.GetWriteQueue(event.srcAddr)
|
||||
|
||||
if writeQueue == nil {
|
||||
log := pfxlog.Logger()
|
||||
origDest, err := getOriginalDest(event.oob)
|
||||
log.Infof("received datagram from %v (original dest %v)", event.srcAddr, origDest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while getting original destination packet: %v", err)
|
||||
}
|
||||
service, err := event.interceptor.interceptLUT.GetByAddress(origDest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pfxlog.Logger().Infof("Creating separate UDP socket with list addr: %v", origDest)
|
||||
packetConn, err := listenConfig.ListenPacket(context.Background(), "udp", origDest.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
writeConn := packetConn.(*net.UDPConn)
|
||||
writeQueue, err = manager.CreateWriteQueue(event.srcAddr, service.Name, writeConn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
pfxlog.Logger().Infof("received %v bytes for conn %v -> %v", len(event.buf.Buf), writeQueue.LocalAddr().String(), writeQueue.Service())
|
||||
writeQueue.Accept(event.buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getOriginalDest(oob []byte) (*net.UDPAddr, error) {
|
||||
cmsgs, err := syscall.ParseSocketControlMessage(oob)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, cmsg := range cmsgs {
|
||||
if cmsg.Header.Level == syscall.SOL_IP && cmsg.Header.Type == syscall.IP_ORIGDSTADDR {
|
||||
ip := cmsg.Data[4:8]
|
||||
port := int(cmsg.Data[2])<<8 + int(cmsg.Data[3])
|
||||
return &net.UDPAddr{IP: ip, Port: port}, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("original destination not found in out of band data")
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) Stop() {
|
||||
log := pfxlog.Logger()
|
||||
err := t.tcpLn.Close()
|
||||
if err != nil {
|
||||
log.Errorf("failed to close TCP listener: %v", err)
|
||||
}
|
||||
|
||||
err = t.udpLn.Close()
|
||||
if err != nil {
|
||||
log.Errorf("failed to close UDP listener: %v", err)
|
||||
}
|
||||
|
||||
var interceptedServices []string
|
||||
for _, svc := range t.interceptLUT {
|
||||
interceptedServices = append(interceptedServices, svc.Name)
|
||||
}
|
||||
for _, svcName := range interceptedServices {
|
||||
err := t.StopIntercepting(svcName, true)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clean up intercept configuration for %s: %v", svcName, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = t.ipt.Delete(mangleTable, srcChain, []string{"-j", dstChain}...)
|
||||
if err != nil {
|
||||
log.Errorf("failed to unlink chain %s: %v", dstChain, err)
|
||||
}
|
||||
|
||||
// this shouldn't be needed after deleting rules one-by-one above, but just in case...
|
||||
err = t.ipt.ClearChain(mangleTable, dstChain)
|
||||
if err != nil {
|
||||
log.Errorf("failed to clear chain %s, %v", dstChain, err)
|
||||
}
|
||||
|
||||
err = t.ipt.DeleteChain(mangleTable, dstChain)
|
||||
if err != nil {
|
||||
log.Errorf("failed to delete chain %s: %v", dstChain, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) Intercept(service edge.Service, resolver dns.Resolver) error {
|
||||
err := t.intercept(service, resolver, (*TCPIPPortAddr)(t.tcpLn.Addr().(*net.TCPAddr)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return t.intercept(service, resolver, (*UDPIPPortAddr)(t.udpLn.LocalAddr().(*net.UDPAddr)))
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) intercept(service edge.Service, resolver dns.Resolver, port IPPortAddr) error {
|
||||
interceptAddr, err := intercept.NewInterceptAddress(service, port.GetProtocol(), resolver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to intercept %s: %v", service.Name, err)
|
||||
}
|
||||
|
||||
ipNet := interceptAddr.IpNet()
|
||||
err = router.AddLocalAddress(&ipNet, "lo")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add local route: %v", err)
|
||||
}
|
||||
|
||||
spec := []string{
|
||||
"-m", "comment", "--comment", service.Name,
|
||||
"-d", ipNet.String(),
|
||||
"-p", interceptAddr.Proto(),
|
||||
"--dport", strconv.Itoa(interceptAddr.Port()),
|
||||
"-j", "TPROXY",
|
||||
"--tproxy-mark", "0x1/0x1",
|
||||
fmt.Sprintf("--on-ip=%s", port.GetIP().String()),
|
||||
fmt.Sprintf("--on-port=%d", port.GetPort()),
|
||||
}
|
||||
|
||||
pfxlog.Logger().Infof("Adding rule iptables -t %v -A %v %v", mangleTable, dstChain, spec)
|
||||
err = t.ipt.AppendUnique(mangleTable, dstChain, spec...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to append rule: %v", err)
|
||||
}
|
||||
|
||||
err = t.interceptLUT.Put(*interceptAddr, service.Name, spec)
|
||||
if err != nil {
|
||||
_ = t.ipt.Delete(mangleTable, dstChain, spec...)
|
||||
return fmt.Errorf("failed to add intercept record: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tProxyInterceptor) StopIntercepting(serviceName string, removeRoute bool) error {
|
||||
service, err := t.interceptLUT.GetByName(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service %s not found in intercept LUT", serviceName)
|
||||
}
|
||||
defer t.interceptLUT.Remove(service.Addr)
|
||||
|
||||
err = t.ipt.Delete(mangleTable, dstChain, service.Data.([]string)...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove iptables rule for service %s: %v", serviceName, err)
|
||||
}
|
||||
|
||||
if removeRoute {
|
||||
ipNet := service.Addr.IpNet()
|
||||
err := router.RemoveLocalAddress(&ipNet, "lo")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to remove route for service %s: %v", serviceName, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type IPPortAddr interface {
|
||||
GetIP() net.IP
|
||||
GetPort() int
|
||||
GetProtocol() string
|
||||
}
|
||||
|
||||
type UDPIPPortAddr net.UDPAddr
|
||||
|
||||
func (addr *UDPIPPortAddr) GetIP() net.IP {
|
||||
return addr.IP
|
||||
}
|
||||
|
||||
func (addr *UDPIPPortAddr) GetPort() int {
|
||||
return addr.Port
|
||||
}
|
||||
|
||||
func (addr *UDPIPPortAddr) GetProtocol() string {
|
||||
return "udp"
|
||||
}
|
||||
|
||||
type TCPIPPortAddr net.TCPAddr
|
||||
|
||||
func (addr *TCPIPPortAddr) GetIP() net.IP {
|
||||
return addr.IP
|
||||
}
|
||||
|
||||
func (addr *TCPIPPortAddr) GetPort() int {
|
||||
return addr.Port
|
||||
}
|
||||
|
||||
func (addr *TCPIPPortAddr) GetProtocol() string {
|
||||
return "tcp"
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package tun
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti/edge"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/dns"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/tcp"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/intercept/protocols/udp"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/router"
|
||||
"github.com/netfoundry/ziti-edge/tunnel/utils"
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type tunInterceptor struct {
|
||||
interceptLUT intercept.LUT
|
||||
tunIf *tunInterface
|
||||
localPrefix *net.IPNet
|
||||
udpManager *udp.TunUDPManager
|
||||
}
|
||||
|
||||
func New(devName string, mtu uint) (intercept.Interceptor, error) {
|
||||
tunIf, err := open(devName, mtu)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open tun interface (name='%s', mtu=%d): %v", devName, mtu, err)
|
||||
}
|
||||
|
||||
la, err := utils.NextIP(net.IP{169, 254, 1, 1}, net.IP{169, 254, 254, 254})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next IP for tun local address: %v", err)
|
||||
}
|
||||
prefix := &net.IPNet{IP: la, Mask: net.CIDRMask(32, 32)}
|
||||
|
||||
err = router.AddLocalAddress(prefix, tunIf.iFace.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := tunInterceptor{
|
||||
interceptLUT: intercept.NewLUT(),
|
||||
tunIf: tunIf,
|
||||
localPrefix: prefix,
|
||||
udpManager: udp.NewManager(tunIf.dev),
|
||||
}
|
||||
|
||||
return &i, nil
|
||||
}
|
||||
|
||||
func (t *tunInterceptor) Start(context ziti.Context) {
|
||||
go protocols.HandleInboundPackets(context, t.tunIf.dev, uint(t.tunIf.iFace.MTU), t.udpManager)
|
||||
}
|
||||
|
||||
func (t *tunInterceptor) Stop() {
|
||||
err := t.tunIf.close()
|
||||
if err != nil {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tunInterceptor) Intercept(service edge.Service, resolver dns.Resolver) error {
|
||||
interceptAddr, err := intercept.NewInterceptAddress(service, "any", resolver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to intercept %s: %v", service.Name, err)
|
||||
}
|
||||
|
||||
ipNet := interceptAddr.IpNet()
|
||||
err = router.AddPointToPointAddress(t.localPrefix.IP, &ipNet, t.tunIf.iFace.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add route %v", err)
|
||||
}
|
||||
|
||||
err = t.interceptLUT.Put(*interceptAddr, service.Name, nil)
|
||||
if err != nil {
|
||||
e := router.RemovePointToPointAddress(t.localPrefix.IP, &ipNet, t.tunIf.iFace.Name)
|
||||
if e != nil {
|
||||
return fmt.Errorf("failed to add service to LUT: %v; failed to clean up local route: %v", err, e)
|
||||
}
|
||||
return fmt.Errorf("failed to add service to LUT: %v", err)
|
||||
}
|
||||
tcp.RegisterService(service, ipNet.IP)
|
||||
t.udpManager.RegisterService(service, ipNet.IP)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tunInterceptor) StopIntercepting(serviceName string, removeRoute bool) error {
|
||||
service, err := t.interceptLUT.GetByName(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("service %s not found in intercept LUT", serviceName)
|
||||
}
|
||||
defer t.interceptLUT.Remove(service.Addr)
|
||||
|
||||
tcp.UnregisterService(serviceName)
|
||||
t.udpManager.UnregisterService(serviceName)
|
||||
|
||||
if removeRoute {
|
||||
ipNet := service.Addr.IpNet()
|
||||
err = router.RemovePointToPointAddress(t.localPrefix.IP, &ipNet, t.tunIf.iFace.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete route: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package tun
|
||||
|
||||
// Create and manipulate tunnel interfaces in Linux.
|
||||
//
|
||||
// References:
|
||||
// - https://backreference.org/2010/03/26/tuntap-interface-tutorial/
|
||||
// - https://medium.com/@mdlayher/linux-netlink-and-go-part-1-netlink-4781aaeeaca8
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/achanda/go-sysctl"
|
||||
"github.com/mdlayher/netlink"
|
||||
"github.com/mdlayher/netlink/nlenc"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"golang.org/x/sys/unix"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
tunCloneDevice = "/dev/net/tun"
|
||||
)
|
||||
|
||||
type tunInterface struct {
|
||||
dev *os.File
|
||||
iFace *net.Interface
|
||||
}
|
||||
|
||||
type ifreqFlags struct {
|
||||
name [unix.IFNAMSIZ]byte
|
||||
flags int32
|
||||
}
|
||||
|
||||
func open(name string, mtu uint) (*tunInterface, error) {
|
||||
const In6AddrGenModeNone = "1"
|
||||
f, err := os.OpenFile(tunCloneDevice, os.O_RDWR, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = f.SetDeadline(time.Time{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunFlags := ifreqFlags{
|
||||
[unix.IFNAMSIZ]byte{},
|
||||
unix.IFF_TUN | unix.IFF_NO_PI, // | unix.IFF_TUN_EXCL,
|
||||
}
|
||||
copy(tunFlags.name[:len(tunFlags.name)-1], []byte(name+"\000"))
|
||||
|
||||
err = ioctl(f, unix.TUNSETIFF, uintptr(unsafe.Pointer(&tunFlags)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ifName := string(bytes.TrimRight(tunFlags.name[:], "\000"))
|
||||
|
||||
// prevent auto-assigned link local IPv6 address when the interface is brought up
|
||||
// https://www.toradex.com/community/questions/16932/ipv6-addrconfnetdev-up-eth0-link-is-not-ready.html
|
||||
if err = sysctl.Set(fmt.Sprintf("net.ipv6.conf.%s.addr_gen_mode", ifName), In6AddrGenModeNone); err != nil {
|
||||
pfxlog.Logger().Warnf("failed setting addr_gen_mode on %s: %v", ifName, err)
|
||||
}
|
||||
|
||||
if err = ioctl(f, unix.TUNSETOWNER, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = ioctl(f, unix.TUNSETGROUP, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tunFlags.flags = int32(mtu)
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM, unix.IPPROTO_IP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sock := os.NewFile(uintptr(fd), "raw")
|
||||
if err = ioctl(sock, unix.SIOCSIFMTU, uintptr(unsafe.Pointer(&tunFlags))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iFace, err := net.InterfaceByName(ifName)
|
||||
tun := &tunInterface{
|
||||
dev: f,
|
||||
iFace: iFace,
|
||||
}
|
||||
|
||||
err = tun.setFlags([]int16{unix.IFF_UP})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tun, nil
|
||||
}
|
||||
|
||||
func marshalIfInfomsg(m *unix.IfInfomsg) []byte {
|
||||
b := make([]byte, unix.SizeofIfInfomsg)
|
||||
|
||||
b[0] = m.Family
|
||||
b[1] = 0 // pad
|
||||
nlenc.PutUint16(b[2:4], m.Type)
|
||||
nlenc.PutInt32(b[4:8], m.Index)
|
||||
nlenc.PutUint32(b[8:12], m.Flags)
|
||||
nlenc.PutUint32(b[12:16], m.Change)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func (tunIf *tunInterface) close() error {
|
||||
setFlagErr := tunIf.setFlags([]int16{^unix.IFF_UP})
|
||||
closeErr := tunIf.dev.Close()
|
||||
|
||||
if setFlagErr == nil {
|
||||
return closeErr
|
||||
}
|
||||
if closeErr == nil {
|
||||
return setFlagErr
|
||||
}
|
||||
return fmt.Errorf("multiple failures while closing tun device: (%v) (%v)", setFlagErr, closeErr)
|
||||
}
|
||||
|
||||
func (tunIf *tunInterface) setFlags(flags []int16) error {
|
||||
c, err := netlink.Dial(unix.AF_UNSPEC, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeNetlink(c)
|
||||
|
||||
var ifFlags, mask uint32
|
||||
for _, f := range flags {
|
||||
ifFlags |= uint32(f)
|
||||
if f < 0 {
|
||||
mask |= uint32(^f)
|
||||
} else {
|
||||
mask |= uint32(f)
|
||||
}
|
||||
}
|
||||
ifmBytes := marshalIfInfomsg(&unix.IfInfomsg{
|
||||
Family: unix.AF_UNSPEC,
|
||||
Type: unix.ARPHRD_NONE,
|
||||
Index: int32(tunIf.iFace.Index),
|
||||
Flags: ifFlags,
|
||||
Change: mask,
|
||||
})
|
||||
|
||||
req := netlink.Message{
|
||||
Header: netlink.Header{
|
||||
Type: unix.RTM_NEWLINK,
|
||||
Flags: unix.NLM_F_REQUEST | unix.NLM_F_ACK,
|
||||
},
|
||||
Data: ifmBytes,
|
||||
}
|
||||
|
||||
msgs, err := c.Execute(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if n := len(msgs); n != 1 {
|
||||
return errors.New("expected 1 netlink message, but got something else")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func closeNetlink(conn *netlink.Conn) {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
pfxlog.Logger().Errorf("failure closing netlink connection (%v)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func ioctl(f *os.File, request uint32, argp uintptr) error {
|
||||
_, _, errno := unix.Syscall(unix.SYS_IOCTL, f.Fd(), uintptr(request), argp)
|
||||
if errno != 0 {
|
||||
return fmt.Errorf("ioctl failed with '%s'", errno)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mdlayher/netlink"
|
||||
"github.com/mdlayher/netlink/nlenc"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"golang.org/x/sys/unix"
|
||||
"net"
|
||||
)
|
||||
|
||||
// Add an address (or prefix) to the specified network interface.
|
||||
func AddLocalAddress(prefix *net.IPNet, ifName string) error {
|
||||
return nlAddrReq(prefix, nil, ifName, unix.RTM_NEWADDR)
|
||||
}
|
||||
|
||||
func RemoveLocalAddress(prefix *net.IPNet, ifName string) error {
|
||||
|
||||
return nlAddrReq(prefix, nil, ifName, unix.RTM_DELADDR)
|
||||
}
|
||||
|
||||
func ipToIPNet(ip net.IP) *net.IPNet {
|
||||
var prefixLen int
|
||||
if ip.To4() != nil {
|
||||
prefixLen = 32
|
||||
} else {
|
||||
prefixLen = 128
|
||||
}
|
||||
|
||||
return &net.IPNet{IP: ip, Mask: net.CIDRMask(prefixLen, prefixLen)}
|
||||
}
|
||||
|
||||
func AddPointToPointAddress(localIP net.IP, peerPrefix *net.IPNet, ifName string) error {
|
||||
return nlAddrReq(ipToIPNet(localIP), peerPrefix, ifName, unix.RTM_NEWADDR)
|
||||
}
|
||||
|
||||
func RemovePointToPointAddress(localIP net.IP, peerPrefix *net.IPNet, ifName string) error {
|
||||
return nlAddrReq(ipToIPNet(localIP), peerPrefix, ifName, unix.RTM_DELADDR)
|
||||
}
|
||||
|
||||
func nlAddrReq(localPrefix, peerPrefix *net.IPNet, ifName string, t netlink.HeaderType) error {
|
||||
netIf, err := net.InterfaceByName(ifName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find interface %s: %v", ifName, err)
|
||||
}
|
||||
|
||||
var localIP net.IP
|
||||
var addrFamily uint8
|
||||
var prefixLen int
|
||||
|
||||
if localPrefix.IP.To4() != nil {
|
||||
localIP = localPrefix.IP.To4()
|
||||
addrFamily = unix.AF_INET
|
||||
} else {
|
||||
localIP = localPrefix.IP
|
||||
addrFamily = unix.AF_INET6
|
||||
}
|
||||
|
||||
rtAttrs := []netlink.Attribute{{Type: unix.IFA_LOCAL, Data: localIP}}
|
||||
|
||||
if peerPrefix == nil {
|
||||
// local address - use prefix length from local
|
||||
prefixLen, _ = localPrefix.Mask.Size()
|
||||
} else {
|
||||
// point-to-point address - use prefix length from peer, and add routing attribute.
|
||||
// see ip-address(8), rtnetlink(7)
|
||||
peerIP := peerPrefix.IP
|
||||
if peerIP.To4() != nil {
|
||||
if addrFamily != unix.AF_INET {
|
||||
return fmt.Errorf("local address '%s' and peer address '%s' have different address family",
|
||||
localIP.String(), peerIP.String())
|
||||
}
|
||||
peerIP = peerIP.To4()
|
||||
}
|
||||
prefixLen, _ = peerPrefix.Mask.Size()
|
||||
rtAttrs = append(rtAttrs, netlink.Attribute{Type: unix.IFA_ADDRESS, Data: peerIP})
|
||||
}
|
||||
|
||||
c, err := netlink.Dial(unix.AF_UNSPEC, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error dialing netlink: %v", err)
|
||||
}
|
||||
defer closeNetlink(c)
|
||||
|
||||
ifmBytes := marshalIfAddrmsg(&unix.IfAddrmsg{
|
||||
Family: addrFamily,
|
||||
Prefixlen: uint8(prefixLen),
|
||||
Scope: unix.RT_SCOPE_HOST,
|
||||
Index: uint32(netIf.Index),
|
||||
})
|
||||
attrBytes, err := netlink.MarshalAttributes(rtAttrs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshalling routing attributes: %v", err)
|
||||
}
|
||||
|
||||
req := netlink.Message{
|
||||
Header: netlink.Header{
|
||||
Type: t,
|
||||
Flags: unix.NLM_F_REQUEST | unix.NLM_F_ACK,
|
||||
},
|
||||
Data: append(ifmBytes, attrBytes...),
|
||||
}
|
||||
|
||||
msgs, err := c.Execute(req)
|
||||
if err == unix.EEXIST {
|
||||
return nil
|
||||
}
|
||||
|
||||
n := len(msgs)
|
||||
if n != 1 {
|
||||
return fmt.Errorf("expected 1 netlink message, but got %d", n)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalIfAddrmsg packs a unix.IfAddrmsg into a byte slice using host byte order.
|
||||
// The returned slice can be included in the payload of a netlink message.
|
||||
func marshalIfAddrmsg(m *unix.IfAddrmsg) []byte {
|
||||
b := make([]byte, unix.SizeofIfAddrmsg)
|
||||
|
||||
b[0] = m.Family
|
||||
b[1] = m.Prefixlen
|
||||
b[2] = m.Flags
|
||||
b[3] = m.Scope
|
||||
nlenc.PutUint32(b[4:8], m.Index)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func marshalIfInfomsg(m *unix.IfInfomsg) []byte {
|
||||
b := make([]byte, unix.SizeofIfInfomsg)
|
||||
|
||||
b[0] = m.Family
|
||||
b[1] = 0 // pad
|
||||
nlenc.PutUint16(b[2:4], m.Type)
|
||||
nlenc.PutInt32(b[4:8], m.Index)
|
||||
nlenc.PutUint32(b[8:12], m.Flags)
|
||||
nlenc.PutUint32(b[12:16], m.Change)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func closeNetlink(conn *netlink.Conn) {
|
||||
err := conn.Close()
|
||||
if err != nil {
|
||||
pfxlog.Logger().Errorf("failure closing netlink connection (%v)", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package router
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
func AddLocalAddress(prefix *net.IPNet, ifName string) error {
|
||||
return errors.New("AddLocalAddress is not implemented on this operating system")
|
||||
}
|
||||
|
||||
func RemoveLocalAddress(prefix *net.IPNet, ifName string) error {
|
||||
return errors.New("RemoveLocalAddress is not implemented on this operating system")
|
||||
}
|
||||
|
||||
func AddPointToPointAddress(localIP net.IP, peerPrefix *net.IPNet, ifName string) error {
|
||||
return errors.New("AddPointToPointAddress is not implemented on this operating system")
|
||||
}
|
||||
|
||||
func RemovePointToPointAddress(localIP net.IP, peerPrefix *net.IPNet, ifName string) error {
|
||||
return errors.New("RemovePointToPointAddress is not implemented on this operating system")
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-foundation/transport/udp"
|
||||
"github.com/sirupsen/logrus"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
var log = logrus.StandardLogger()
|
||||
|
||||
func Run(context ziti.Context, service string, clientConn net.Conn) {
|
||||
zitiConn, err := context.Dial(service)
|
||||
if err != nil {
|
||||
log.Errorf("zt.Dial(%s) failed: %s", service, err.Error())
|
||||
clientConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
loggerFields := logrus.Fields{
|
||||
"src-remote": clientConn.RemoteAddr(), "src-local": clientConn.LocalAddr(),
|
||||
"dst-local": zitiConn.LocalAddr(), "dst-remote": zitiConn.RemoteAddr()}
|
||||
|
||||
log := log.WithFields(loggerFields)
|
||||
log.Info("tunnel started")
|
||||
|
||||
doneSend := make(chan int64)
|
||||
doneRecv := make(chan int64)
|
||||
|
||||
go myCopy(clientConn, zitiConn, doneSend)
|
||||
|
||||
go myCopy(zitiConn, clientConn, doneRecv)
|
||||
|
||||
var n1, n2 int64
|
||||
for count := 2; count > 0; {
|
||||
select {
|
||||
case n1 = <-doneSend:
|
||||
case n2 = <-doneRecv:
|
||||
}
|
||||
count = count - 1
|
||||
}
|
||||
|
||||
log.Infof("tunnel closed: %d bytes sent; %d bytes received", n2, n1)
|
||||
}
|
||||
|
||||
func myCopy(dst net.Conn, src net.Conn, done chan int64) {
|
||||
loggerFields := logrus.Fields{
|
||||
"src-remote": src.RemoteAddr(), "src-local": src.LocalAddr(),
|
||||
"dst-local": dst.LocalAddr(), "dst-remote": dst.RemoteAddr()}
|
||||
|
||||
defer dst.Close()
|
||||
defer log.WithFields(loggerFields).Info("stopping pipe")
|
||||
copyBuf := make([]byte, udp.MaxPacketSize)
|
||||
n, err := io.CopyBuffer(dst, src, copyBuf)
|
||||
done <- n
|
||||
|
||||
if err != nil {
|
||||
log.WithFields(loggerFields).Error(err.Error())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package udp_vconn
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-foundation/util/mempool"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type UDPWriterTo interface {
|
||||
WriteTo(b []byte, addr net.Addr) (int, error)
|
||||
LocalAddr() net.Addr
|
||||
}
|
||||
|
||||
type Event interface {
|
||||
Handle(Manager) error
|
||||
}
|
||||
|
||||
type Manager interface {
|
||||
GetWriteQueue(clientAddr net.Addr) WriteQueue
|
||||
CreateWriteQueue(clientAddr net.Addr, service string, conn UDPWriterTo) (WriteQueue, error)
|
||||
QueueEvent(Event)
|
||||
QueueError(error)
|
||||
}
|
||||
|
||||
type WriteQueue interface {
|
||||
Accept(mempool.PooledBuffer)
|
||||
LocalAddr() net.Addr
|
||||
Service() string
|
||||
}
|
||||
|
||||
type NewConnAcceptResult int
|
||||
|
||||
const (
|
||||
Allow NewConnAcceptResult = iota
|
||||
Deny
|
||||
AllowDropLRU
|
||||
)
|
||||
|
||||
type NewConnPolicy interface {
|
||||
NewConnection(currentCount uint32) NewConnAcceptResult
|
||||
}
|
||||
|
||||
type ConnExpirationPolicy interface {
|
||||
IsExpired(now, lastUsed time.Time) bool
|
||||
PollFrequency() time.Duration
|
||||
}
|
||||
|
||||
type UnpooledBuffer []byte
|
||||
|
||||
func (u UnpooledBuffer) GetPayload() []byte {
|
||||
return u
|
||||
}
|
||||
|
||||
func (u UnpooledBuffer) Release() {
|
||||
// does nothing
|
||||
}
|
||||
|
||||
func NewManager(context ziti.Context, newConnPolicy NewConnPolicy, expirationPolicy ConnExpirationPolicy) Manager {
|
||||
manager := &manager{
|
||||
eventC: make(chan Event),
|
||||
context: context,
|
||||
connMap: make(map[string]*udpConn),
|
||||
newConnPolicy: newConnPolicy,
|
||||
expirationPolicy: expirationPolicy,
|
||||
}
|
||||
|
||||
go manager.run()
|
||||
return manager
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package udp_vconn
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-foundation/util/mempool"
|
||||
"errors"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type udpConn struct {
|
||||
readC chan mempool.PooledBuffer
|
||||
service string
|
||||
srcAddr net.Addr
|
||||
manager *manager
|
||||
writeConn UDPWriterTo
|
||||
lastUse time.Time
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (conn *udpConn) Service() string {
|
||||
return conn.service
|
||||
}
|
||||
|
||||
func (conn *udpConn) Accept(buffer mempool.PooledBuffer) {
|
||||
conn.readC <- buffer
|
||||
}
|
||||
|
||||
func (conn *udpConn) Network() string {
|
||||
return "ziti"
|
||||
}
|
||||
|
||||
func (conn *udpConn) String() string {
|
||||
return conn.service
|
||||
}
|
||||
|
||||
func (conn *udpConn) WriteTo(w io.Writer) (n int64, err error) {
|
||||
var bytesWritten int64
|
||||
for {
|
||||
buf, ok := <-conn.readC
|
||||
|
||||
if !ok {
|
||||
return bytesWritten, io.EOF
|
||||
}
|
||||
|
||||
n, err := w.Write(buf.GetPayload())
|
||||
buf.Release()
|
||||
conn.lastUse = time.Now()
|
||||
bytesWritten += int64(n)
|
||||
if err != nil {
|
||||
return bytesWritten, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (conn *udpConn) Read(b []byte) (n int, err error) {
|
||||
return 0, errors.New("read not implemented, assuming we always want WriteTo used instead")
|
||||
}
|
||||
|
||||
func (conn *udpConn) Write(b []byte) (int, error) {
|
||||
pfxlog.Logger().Infof("Received %v bytes from ziti to return to udp", len(b))
|
||||
// TODO: UDP chunking, MTU chunking?
|
||||
n, err := conn.writeConn.WriteTo(b, conn.srcAddr)
|
||||
conn.lastUse = time.Now()
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (conn *udpConn) Close() error {
|
||||
conn.manager.queueClose(conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *udpConn) LocalAddr() net.Addr {
|
||||
return conn.srcAddr
|
||||
}
|
||||
|
||||
func (conn *udpConn) RemoteAddr() net.Addr {
|
||||
return conn
|
||||
}
|
||||
|
||||
func (conn *udpConn) SetDeadline(t time.Time) error {
|
||||
// ignore, since this is a shared connection
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *udpConn) SetReadDeadline(t time.Time) error {
|
||||
// ignore, since this is a shared connection
|
||||
return nil
|
||||
}
|
||||
|
||||
func (conn *udpConn) SetWriteDeadline(t time.Time) error {
|
||||
// ignore, since this is a shared connection
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package udp_vconn
|
||||
|
||||
import (
|
||||
"github.com/netfoundry/ziti-edge/sdk/ziti"
|
||||
"github.com/netfoundry/ziti-edge/tunnel"
|
||||
"github.com/netfoundry/ziti-foundation/util/mempool"
|
||||
"errors"
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"io"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type manager struct {
|
||||
eventC chan Event
|
||||
context ziti.Context
|
||||
connMap map[string]*udpConn
|
||||
newConnPolicy NewConnPolicy
|
||||
expirationPolicy ConnExpirationPolicy
|
||||
}
|
||||
|
||||
func (manager *manager) QueueEvent(event Event) {
|
||||
manager.eventC <- event
|
||||
}
|
||||
|
||||
func (manager *manager) QueueError(err error) {
|
||||
manager.QueueEvent(&errorEvent{err})
|
||||
}
|
||||
|
||||
func (manager *manager) run() {
|
||||
log := pfxlog.Logger()
|
||||
defer log.Info("shutting down udp listener")
|
||||
|
||||
timer := time.NewTicker(manager.expirationPolicy.PollFrequency())
|
||||
defer timer.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-manager.eventC:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
err := event.Handle(manager)
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Errorf("error while handling udp event: %v", err)
|
||||
}
|
||||
case <-timer.C:
|
||||
manager.dropExpired()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *manager) GetWriteQueue(srcAddr net.Addr) WriteQueue {
|
||||
pfxlog.Logger().Infof("Looking up address %v", srcAddr.String())
|
||||
result := manager.connMap[srcAddr.String()]
|
||||
if result == nil {
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (manager *manager) CreateWriteQueue(srcAddr net.Addr, service string, writeConn UDPWriterTo) (WriteQueue, error) {
|
||||
switch manager.newConnPolicy.NewConnection(uint32(len(manager.connMap))) {
|
||||
case AllowDropLRU:
|
||||
manager.dropLRU()
|
||||
case Deny:
|
||||
return nil, errors.New("max connections exceeded")
|
||||
}
|
||||
conn := &udpConn{
|
||||
readC: make(chan mempool.PooledBuffer),
|
||||
service: service,
|
||||
srcAddr: srcAddr,
|
||||
manager: manager,
|
||||
writeConn: writeConn,
|
||||
lastUse: time.Now(),
|
||||
}
|
||||
manager.connMap[srcAddr.String()] = conn
|
||||
pfxlog.Logger().Infof("created new virtual UDP connection for %v", srcAddr.String())
|
||||
go tunnel.Run(manager.context, service, conn)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (manager *manager) dropLRU() {
|
||||
if len(manager.connMap) < 1 {
|
||||
return
|
||||
}
|
||||
var oldest *udpConn
|
||||
for _, value := range manager.connMap {
|
||||
if oldest == nil {
|
||||
oldest = value
|
||||
} else {
|
||||
if oldest.lastUse.After(value.lastUse) {
|
||||
oldest = value
|
||||
}
|
||||
}
|
||||
}
|
||||
manager.close(oldest)
|
||||
}
|
||||
|
||||
func (manager *manager) dropExpired() {
|
||||
log := pfxlog.Logger()
|
||||
now := time.Now()
|
||||
for key, value := range manager.connMap {
|
||||
if manager.expirationPolicy.IsExpired(now, value.lastUse) {
|
||||
log.Infof("connection %v expired. removing from UDP vconn manager", key)
|
||||
manager.close(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *manager) queueClose(conn *udpConn) {
|
||||
// this will likely get called from the event loop, so make sure we don't deadlock
|
||||
go manager.QueueEvent(&closeEvent{manager, conn})
|
||||
}
|
||||
|
||||
func (manager *manager) close(conn *udpConn) {
|
||||
if !conn.closed {
|
||||
conn.closed = true
|
||||
delete(manager.connMap, conn.srcAddr.String())
|
||||
close(conn.readC)
|
||||
}
|
||||
}
|
||||
|
||||
type closeEvent struct {
|
||||
manager *manager
|
||||
conn *udpConn
|
||||
}
|
||||
|
||||
func (event *closeEvent) Handle(manager Manager) error {
|
||||
event.manager.close(event.conn)
|
||||
return nil
|
||||
}
|
||||
|
||||
type errorEvent struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (event *errorEvent) Handle(Manager) error {
|
||||
return event
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package udp_vconn
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewUnlimitedConnectionPolicy() NewConnPolicy {
|
||||
return unlimitedConnections{}
|
||||
}
|
||||
|
||||
type unlimitedConnections struct{}
|
||||
|
||||
func (policy unlimitedConnections) NewConnection(count uint32) NewConnAcceptResult {
|
||||
return Allow
|
||||
}
|
||||
|
||||
func NewLimitedConnectionPolicyDropNew(limit uint32) NewConnPolicy {
|
||||
return &limitedConnections{result: Deny, limit: limit}
|
||||
}
|
||||
|
||||
func NewLimitedConnectionPolicyDropLRU(limit uint32) NewConnPolicy {
|
||||
return &limitedConnections{result: AllowDropLRU, limit: limit}
|
||||
}
|
||||
|
||||
type limitedConnections struct {
|
||||
result NewConnAcceptResult
|
||||
limit uint32
|
||||
}
|
||||
|
||||
func (policy *limitedConnections) NewConnection(count uint32) NewConnAcceptResult {
|
||||
if count >= policy.limit {
|
||||
return policy.result
|
||||
}
|
||||
return Allow
|
||||
}
|
||||
|
||||
func NewDefaultExpirationPolicy() ConnExpirationPolicy {
|
||||
return defaultExpirationPolicy{}
|
||||
}
|
||||
|
||||
type defaultExpirationPolicy struct {
|
||||
}
|
||||
|
||||
func (policy defaultExpirationPolicy) IsExpired(now, lastUsed time.Time) bool {
|
||||
return now.Sub(lastUsed) > time.Minute*10
|
||||
}
|
||||
|
||||
func (policy defaultExpirationPolicy) PollFrequency() time.Duration {
|
||||
return time.Minute * 1
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// Return all IP addresses that are currently in use on the local host.
|
||||
//
|
||||
// This function differs from net.InterfaceAddrs() in that peer addresses
|
||||
// for point-to-point interfaces are returned by this function, in addition
|
||||
// to local interface addresses.
|
||||
func AllInterfaceAddrs() ([]net.Addr, error) {
|
||||
tab, err := syscall.NetlinkRIB(syscall.RTM_GETADDR, syscall.AF_INET)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("netlinkrib", err)
|
||||
}
|
||||
msgs, err := syscall.ParseNetlinkMessage(tab)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("parsenetlinkmessage", err)
|
||||
}
|
||||
|
||||
var ifat []net.Addr
|
||||
loop:
|
||||
for _, m := range msgs {
|
||||
switch m.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
break loop
|
||||
case syscall.RTM_NEWADDR:
|
||||
ifam := (*syscall.IfAddrmsg)(unsafe.Pointer(&m.Data[0]))
|
||||
attrs, err := syscall.ParseNetlinkRouteAttr(&m)
|
||||
if err != nil {
|
||||
return nil, os.NewSyscallError("parsenetlinkrouteattr", err)
|
||||
}
|
||||
ifa := newAddr(ifam, attrs)
|
||||
if ifa != nil {
|
||||
for _, a := range ifa {
|
||||
ifat = append(ifat, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ifat, nil
|
||||
}
|
||||
|
||||
func newAddr(ifam *syscall.IfAddrmsg, attrs []syscall.NetlinkRouteAttr) []net.Addr {
|
||||
var addrs []net.Addr
|
||||
for _, a := range attrs {
|
||||
switch a.Attr.Type {
|
||||
case syscall.IFA_ADDRESS:
|
||||
fallthrough
|
||||
case syscall.IFA_LOCAL:
|
||||
switch ifam.Family {
|
||||
case syscall.AF_INET:
|
||||
addr := &net.IPNet{IP: net.IPv4(a.Value[0], a.Value[1], a.Value[2], a.Value[3]), Mask: net.CIDRMask(int(ifam.Prefixlen), 8*net.IPv4len)}
|
||||
addrs = append(addrs, addr)
|
||||
case syscall.AF_INET6:
|
||||
addr := &net.IPNet{IP: make(net.IP, net.IPv6len), Mask: net.CIDRMask(int(ifam.Prefixlen), 8*net.IPv6len)}
|
||||
copy(addr.IP, a.Value[:])
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return addrs
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/michaelquigley/pfxlog"
|
||||
"net"
|
||||
)
|
||||
|
||||
func AllInterfaceAddrs() ([]net.Addr, error) {
|
||||
log := pfxlog.Logger()
|
||||
log.Fatal("utils.AllInterfaceAddrs is not implemented on this operating system")
|
||||
return nil, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2019 Netfoundry, Inc.
|
||||
|
||||
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
|
||||
|
||||
https://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.
|
||||
*/
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
// return the next available IP address in the range of provided IPs
|
||||
func NextIP(lower, upper net.IP) (net.IP, error) {
|
||||
usedAddrs, err := AllInterfaceAddrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for ip := lower; !ip.Equal(upper); incIP(ip) {
|
||||
inUse := false
|
||||
for _, usedAddr := range usedAddrs {
|
||||
usedIP, _, _ := net.ParseCIDR(usedAddr.String())
|
||||
if ip.Equal(usedIP) {
|
||||
inUse = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !inUse {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func incIP(ip net.IP) {
|
||||
for i := len(ip) - 1; i >= 0; i-- {
|
||||
ip[i]++
|
||||
if ip[i] > 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user