Initial migration from internal repositories.

This commit is contained in:
Michael Quigley
2019-12-03 12:13:27 -05:00
parent 12cf269615
commit 58fdd03351
69 changed files with 9126 additions and 0 deletions
+201
View File
@@ -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.
+84
View File
@@ -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.
+194
View File
@@ -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.
+536
View File
@@ -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
```
+165
View File
@@ -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"
}
}
```
+55
View File
@@ -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)
}
}
+69
View File
@@ -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")
}
}
+21
View File
@@ -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
)
+135
View File
@@ -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=
+38
View File
@@ -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
```
+25
View File
@@ -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()
}
+157
View File
@@ -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
}
}
+35
View File
@@ -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))
},
}
+32
View File
@@ -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()
}
+165
View File
@@ -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
}
+60
View File
@@ -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)
}
}
+35
View File
@@ -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))
},
}
+52
View File
@@ -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
}
+117
View File
@@ -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()
}
+33
View File
@@ -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())
}
+160
View File
@@ -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")
}
}
+25
View File
@@ -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) {
}
+230
View File
@@ -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
}
+209
View File
@@ -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()
}
+33
View File
@@ -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))
}
+34
View File
@@ -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)
}
+99
View File
@@ -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
}
+55
View File
@@ -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)
}
}
+442
View File
@@ -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
}
+300
View File
@@ -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)
}
}
+78
View File
@@ -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()
}
+67
View File
@@ -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
}
+56
View File
@@ -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]))
}
}
+460
View File
@@ -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)
}
}
+276
View File
@@ -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
+83
View File
@@ -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
}
+25
View File
@@ -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
}
+177
View File
@@ -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()
}
+160
View File
@@ -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)
}
+54
View File
@@ -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
}
+6
View File
@@ -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.
+59
View File
@@ -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)
}
+48
View File
@@ -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
}
+283
View File
@@ -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
}
+87
View File
@@ -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()
}
}
}
+402
View File
@@ -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]
}
+401
View File
@@ -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
}
+114
View File
@@ -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
}
+121
View File
@@ -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
}
+100
View File
@@ -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)
}
+134
View File
@@ -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
}
+206
View File
@@ -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
}
+202
View File
@@ -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)
}
}
+391
View File
@@ -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"
}
+123
View File
@@ -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
}
+200
View File
@@ -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
}
+165
View File
@@ -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)
}
}
+40
View File
@@ -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")
}
+77
View File
@@ -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())
}
}
+87
View File
@@ -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
}
+111
View File
@@ -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
}
+160
View File
@@ -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
}
+68
View File
@@ -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
}
+83
View File
@@ -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
}
+30
View File
@@ -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
}
+54
View File
@@ -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
}
}
}