diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/doc/001-overview.md b/doc/001-overview.md new file mode 100644 index 000000000..95388d926 --- /dev/null +++ b/doc/001-overview.md @@ -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. \ No newline at end of file diff --git a/doc/002-local-dev.md b/doc/002-local-dev.md new file mode 100644 index 000000000..321a64e3b --- /dev/null +++ b/doc/002-local-dev.md @@ -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": "" +} +``` + +...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/ +``` + +...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 $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. \ No newline at end of file diff --git a/doc/003-local-deploy.md b/doc/003-local-deploy.md new file mode 100644 index 000000000..5029fbac4 --- /dev/null +++ b/doc/003-local-deploy.md @@ -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 \ +--hosted +--hosted-ids +--tags tunneler.dial.addr=
+``` + +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 +``` + diff --git a/doc/breaking-changes-0.6.md b/doc/breaking-changes-0.6.md new file mode 100644 index 000000000..b575c0dc9 --- /dev/null +++ b/doc/breaking-changes-0.6.md @@ -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: + +``` + + "/enroll?token=" + +``` + +# 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" + } +} +``` + + \ No newline at end of file diff --git a/example/curlz/curlz.go b/example/curlz/curlz.go new file mode 100644 index 000000000..924565b1e --- /dev/null +++ b/example/curlz/curlz.go @@ -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) + } +} diff --git a/example/simple-server/simple-server.go b/example/simple-server/simple-server.go new file mode 100644 index 000000000..038d447a0 --- /dev/null +++ b/example/simple-server/simple-server.go @@ -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") + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..d6a025bfb --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..354de72b9 --- /dev/null +++ b/go.sum @@ -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= diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 000000000..98e203015 --- /dev/null +++ b/sdk/README.md @@ -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 : [:]* +``` +`ZITI_SDK_CONFIG` enviroment has to be set. + +Example: +``` +$ export ZITI_SDK_CONFIG=device.json +$ ziti-proxy netcat:33169 +``` + + diff --git a/sdk/cmd/ziti-enroller/main.go b/sdk/cmd/ziti-enroller/main.go new file mode 100644 index 000000000..acbbf9ce5 --- /dev/null +++ b/sdk/cmd/ziti-enroller/main.go @@ -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() +} diff --git a/sdk/cmd/ziti-enroller/subcmd/root.go b/sdk/cmd/ziti-enroller/subcmd/root.go new file mode 100644 index 000000000..49f84bf08 --- /dev/null +++ b/sdk/cmd/ziti-enroller/subcmd/root.go @@ -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 + } +} diff --git a/sdk/cmd/ziti-enroller/subcmd/version.go b/sdk/cmd/ziti-enroller/subcmd/version.go new file mode 100644 index 000000000..fb4b517f9 --- /dev/null +++ b/sdk/cmd/ziti-enroller/subcmd/version.go @@ -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)) + }, +} diff --git a/sdk/cmd/ziti-proxy/main.go b/sdk/cmd/ziti-proxy/main.go new file mode 100644 index 000000000..59b0e81c5 --- /dev/null +++ b/sdk/cmd/ziti-proxy/main.go @@ -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() +} diff --git a/sdk/cmd/ziti-proxy/subcmd/proxy.go b/sdk/cmd/ziti-proxy/subcmd/proxy.go new file mode 100644 index 000000000..68da11760 --- /dev/null +++ b/sdk/cmd/ziti-proxy/subcmd/proxy.go @@ -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 [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 +} diff --git a/sdk/cmd/ziti-proxy/subcmd/root.go b/sdk/cmd/ziti-proxy/subcmd/root.go new file mode 100644 index 000000000..8dd78c1e0 --- /dev/null +++ b/sdk/cmd/ziti-proxy/subcmd/root.go @@ -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) + } +} diff --git a/sdk/cmd/ziti-proxy/subcmd/version.go b/sdk/cmd/ziti-proxy/subcmd/version.go new file mode 100644 index 000000000..a261f617b --- /dev/null +++ b/sdk/cmd/ziti-proxy/subcmd/version.go @@ -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)) + }, +} diff --git a/sdk/ziti/config/config.go b/sdk/ziti/config/config.go new file mode 100644 index 000000000..540216c54 --- /dev/null +++ b/sdk/ziti/config/config.go @@ -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 +} diff --git a/sdk/ziti/config/token.go b/sdk/ziti/config/token.go new file mode 100644 index 000000000..ba61aa073 --- /dev/null +++ b/sdk/ziti/config/token.go @@ -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() +} diff --git a/sdk/ziti/edge/addr.go b/sdk/ziti/edge/addr.go new file mode 100644 index 000000000..993ee7603 --- /dev/null +++ b/sdk/ziti/edge/addr.go @@ -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()) +} diff --git a/sdk/ziti/edge/conn.go b/sdk/ziti/edge/conn.go new file mode 100644 index 000000000..efb4fa595 --- /dev/null +++ b/sdk/ziti/edge/conn.go @@ -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") + } +} \ No newline at end of file diff --git a/sdk/ziti/edge/conn_test.go b/sdk/ziti/edge/conn_test.go new file mode 100644 index 000000000..76592df9f --- /dev/null +++ b/sdk/ziti/edge/conn_test.go @@ -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) { + +} diff --git a/sdk/ziti/edge/messages.go b/sdk/ziti/edge/messages.go new file mode 100644 index 000000000..1c9c1f994 --- /dev/null +++ b/sdk/ziti/edge/messages.go @@ -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 +} diff --git a/sdk/ziti/edge/msg_mux.go b/sdk/ziti/edge/msg_mux.go new file mode 100644 index 000000000..10e1e7e64 --- /dev/null +++ b/sdk/ziti/edge/msg_mux.go @@ -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() +} diff --git a/sdk/ziti/edge/msg_mux_test.go b/sdk/ziti/edge/msg_mux_test.go new file mode 100644 index 000000000..dba021f49 --- /dev/null +++ b/sdk/ziti/edge/msg_mux_test.go @@ -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)) +} diff --git a/sdk/ziti/edge/receive_adapter.go b/sdk/ziti/edge/receive_adapter.go new file mode 100644 index 000000000..6a3d6315a --- /dev/null +++ b/sdk/ziti/edge/receive_adapter.go @@ -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) +} diff --git a/sdk/ziti/edge/types.go b/sdk/ziti/edge/types.go new file mode 100644 index 000000000..61c3656b5 --- /dev/null +++ b/sdk/ziti/edge/types.go @@ -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 +} diff --git a/sdk/ziti/edge/types_test.go b/sdk/ziti/edge/types_test.go new file mode 100644 index 000000000..a62ca9a6f --- /dev/null +++ b/sdk/ziti/edge/types_test.go @@ -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) + } +} diff --git a/sdk/ziti/enroll/enroll.go b/sdk/ziti/enroll/enroll.go new file mode 100644 index 000000000..eff44fd3f --- /dev/null +++ b/sdk/ziti/enroll/enroll.go @@ -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 +} diff --git a/sdk/ziti/internal/edge_impl/conn.go b/sdk/ziti/internal/edge_impl/conn.go new file mode 100644 index 000000000..37403a9ad --- /dev/null +++ b/sdk/ziti/internal/edge_impl/conn.go @@ -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) + } +} diff --git a/sdk/ziti/internal/edge_impl/factory.go b/sdk/ziti/internal/edge_impl/factory.go new file mode 100644 index 000000000..f53156e1f --- /dev/null +++ b/sdk/ziti/internal/edge_impl/factory.go @@ -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() +} \ No newline at end of file diff --git a/sdk/ziti/internal/edge_impl/listener.go b/sdk/ziti/internal/edge_impl/listener.go new file mode 100644 index 000000000..81d12b2f0 --- /dev/null +++ b/sdk/ziti/internal/edge_impl/listener.go @@ -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 +} diff --git a/sdk/ziti/sample/sample.go b/sdk/ziti/sample/sample.go new file mode 100644 index 000000000..b5c64ba5c --- /dev/null +++ b/sdk/ziti/sample/sample.go @@ -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 + 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])) + } +} diff --git a/sdk/ziti/ziti.go b/sdk/ziti/ziti.go new file mode 100644 index 000000000..96958e3b3 --- /dev/null +++ b/sdk/ziti/ziti.go @@ -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) + } +} diff --git a/tunnel/README.md b/tunnel/README.md new file mode 100644 index 000000000..abf593578 --- /dev/null +++ b/tunnel/README.md @@ -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: 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 \ No newline at end of file diff --git a/tunnel/deployment/systemd/etc/systemd/system/ziti-tunnel.env b/tunnel/deployment/systemd/etc/systemd/system/ziti-tunnel.env new file mode 100644 index 000000000..6f09c127b --- /dev/null +++ b/tunnel/deployment/systemd/etc/systemd/system/ziti-tunnel.env @@ -0,0 +1,3 @@ +IDENTITY_JSON=/opt/netfoundry/etc/ziti-client-identity.json +RESOLVER_OPTS=none +INTERCEPT_MODE=tproxy diff --git a/tunnel/deployment/systemd/etc/systemd/system/ziti-tunnel.service b/tunnel/deployment/systemd/etc/systemd/system/ziti-tunnel.service new file mode 100644 index 000000000..127fa47b3 --- /dev/null +++ b/tunnel/deployment/systemd/etc/systemd/system/ziti-tunnel.service @@ -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 \ No newline at end of file diff --git a/tunnel/deployment/sysvinit/etc/init.d/ziti-tunnel b/tunnel/deployment/sysvinit/etc/init.d/ziti-tunnel new file mode 100644 index 000000000..40cfd06d5 --- /dev/null +++ b/tunnel/deployment/sysvinit/etc/init.d/ziti-tunnel @@ -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 diff --git a/tunnel/dns/file.go b/tunnel/dns/file.go new file mode 100644 index 000000000..ce54befe3 --- /dev/null +++ b/tunnel/dns/file.go @@ -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 +} \ No newline at end of file diff --git a/tunnel/dns/resolver.go b/tunnel/dns/resolver.go new file mode 100644 index 000000000..a5322aa09 --- /dev/null +++ b/tunnel/dns/resolver.go @@ -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 +} diff --git a/tunnel/dns/server.go b/tunnel/dns/server.go new file mode 100644 index 000000000..7cef88b50 --- /dev/null +++ b/tunnel/dns/server.go @@ -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() +} diff --git a/tunnel/intercept/interceptor.go b/tunnel/intercept/interceptor.go new file mode 100644 index 000000000..2bbd1a1c5 --- /dev/null +++ b/tunnel/intercept/interceptor.go @@ -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) +} \ No newline at end of file diff --git a/tunnel/intercept/iputils.go b/tunnel/intercept/iputils.go new file mode 100644 index 000000000..404646f08 --- /dev/null +++ b/tunnel/intercept/iputils.go @@ -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 +} diff --git a/tunnel/intercept/protocols/README.md b/tunnel/intercept/protocols/README.md new file mode 100644 index 000000000..de77c5bd7 --- /dev/null +++ b/tunnel/intercept/protocols/README.md @@ -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. \ No newline at end of file diff --git a/tunnel/intercept/protocols/ip/checksum.go b/tunnel/intercept/protocols/ip/checksum.go new file mode 100644 index 000000000..fd01a2310 --- /dev/null +++ b/tunnel/intercept/protocols/ip/checksum.go @@ -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) +} diff --git a/tunnel/intercept/protocols/ip/decode.go b/tunnel/intercept/protocols/ip/decode.go new file mode 100644 index 000000000..31b31599c --- /dev/null +++ b/tunnel/intercept/protocols/ip/decode.go @@ -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 +} \ No newline at end of file diff --git a/tunnel/intercept/protocols/ip/ipv4.go b/tunnel/intercept/protocols/ip/ipv4.go new file mode 100644 index 000000000..c89d6180b --- /dev/null +++ b/tunnel/intercept/protocols/ip/ipv4.go @@ -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 +} diff --git a/tunnel/intercept/protocols/reader.go b/tunnel/intercept/protocols/reader.go new file mode 100644 index 000000000..5a5743113 --- /dev/null +++ b/tunnel/intercept/protocols/reader.go @@ -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() + } + } +} diff --git a/tunnel/intercept/protocols/tcp/conn.go b/tunnel/intercept/protocols/tcp/conn.go new file mode 100644 index 000000000..29e95dd0c --- /dev/null +++ b/tunnel/intercept/protocols/tcp/conn.go @@ -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)<= 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] +} diff --git a/tunnel/intercept/protocols/tcp/header.go b/tunnel/intercept/protocols/tcp/header.go new file mode 100644 index 000000000..6ebcd2c4a --- /dev/null +++ b/tunnel/intercept/protocols/tcp/header.go @@ -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 +} diff --git a/tunnel/intercept/protocols/tcp/proxy.go b/tunnel/intercept/protocols/tcp/proxy.go new file mode 100644 index 000000000..f21f44929 --- /dev/null +++ b/tunnel/intercept/protocols/tcp/proxy.go @@ -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 +} diff --git a/tunnel/intercept/protocols/udp/conn.go b/tunnel/intercept/protocols/udp/conn.go new file mode 100644 index 000000000..390da96d4 --- /dev/null +++ b/tunnel/intercept/protocols/udp/conn.go @@ -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 +} diff --git a/tunnel/intercept/protocols/udp/header.go b/tunnel/intercept/protocols/udp/header.go new file mode 100644 index 000000000..f300a4e73 --- /dev/null +++ b/tunnel/intercept/protocols/udp/header.go @@ -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) +} diff --git a/tunnel/intercept/protocols/udp/proxy.go b/tunnel/intercept/protocols/udp/proxy.go new file mode 100644 index 000000000..6fda34cfe --- /dev/null +++ b/tunnel/intercept/protocols/udp/proxy.go @@ -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 +} diff --git a/tunnel/intercept/proxy/proxy.go b/tunnel/intercept/proxy/proxy.go new file mode 100644 index 000000000..e2b575888 --- /dev/null +++ b/tunnel/intercept/proxy/proxy.go @@ -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 +} \ No newline at end of file diff --git a/tunnel/intercept/svcpoll.go b/tunnel/intercept/svcpoll.go new file mode 100644 index 000000000..a089243e5 --- /dev/null +++ b/tunnel/intercept/svcpoll.go @@ -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) + } +} diff --git a/tunnel/intercept/tproxy/tproxy_linux.go b/tunnel/intercept/tproxy/tproxy_linux.go new file mode 100644 index 000000000..46d11ebb9 --- /dev/null +++ b/tunnel/intercept/tproxy/tproxy_linux.go @@ -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" +} diff --git a/tunnel/intercept/tun/tun.go b/tunnel/intercept/tun/tun.go new file mode 100644 index 000000000..13f4cee49 --- /dev/null +++ b/tunnel/intercept/tun/tun.go @@ -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 +} diff --git a/tunnel/intercept/tun/tunif_linux.go b/tunnel/intercept/tun/tunif_linux.go new file mode 100644 index 000000000..5a817cc23 --- /dev/null +++ b/tunnel/intercept/tun/tunif_linux.go @@ -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 +} diff --git a/tunnel/router/router_linux.go b/tunnel/router/router_linux.go new file mode 100644 index 000000000..5042747f6 --- /dev/null +++ b/tunnel/router/router_linux.go @@ -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) + } +} diff --git a/tunnel/router/router_other.go b/tunnel/router/router_other.go new file mode 100644 index 000000000..2f5c61d17 --- /dev/null +++ b/tunnel/router/router_other.go @@ -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") +} diff --git a/tunnel/tunnel.go b/tunnel/tunnel.go new file mode 100644 index 000000000..0a55aaad3 --- /dev/null +++ b/tunnel/tunnel.go @@ -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()) + } +} diff --git a/tunnel/udp_vconn/api.go b/tunnel/udp_vconn/api.go new file mode 100644 index 000000000..6b65ddc8e --- /dev/null +++ b/tunnel/udp_vconn/api.go @@ -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 +} + diff --git a/tunnel/udp_vconn/conn.go b/tunnel/udp_vconn/conn.go new file mode 100644 index 000000000..6d608820e --- /dev/null +++ b/tunnel/udp_vconn/conn.go @@ -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 +} diff --git a/tunnel/udp_vconn/manager.go b/tunnel/udp_vconn/manager.go new file mode 100644 index 000000000..31ab0e0b5 --- /dev/null +++ b/tunnel/udp_vconn/manager.go @@ -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 +} \ No newline at end of file diff --git a/tunnel/udp_vconn/policy.go b/tunnel/udp_vconn/policy.go new file mode 100644 index 000000000..70b309052 --- /dev/null +++ b/tunnel/udp_vconn/policy.go @@ -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 +} + + diff --git a/tunnel/utils/ifaddrs_linux.go b/tunnel/utils/ifaddrs_linux.go new file mode 100644 index 000000000..b3bf547db --- /dev/null +++ b/tunnel/utils/ifaddrs_linux.go @@ -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 +} diff --git a/tunnel/utils/ifaddrs_other.go b/tunnel/utils/ifaddrs_other.go new file mode 100644 index 000000000..00666a617 --- /dev/null +++ b/tunnel/utils/ifaddrs_other.go @@ -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 +} diff --git a/tunnel/utils/ipcalc.go b/tunnel/utils/ipcalc.go new file mode 100644 index 000000000..2ddd13690 --- /dev/null +++ b/tunnel/utils/ipcalc.go @@ -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 + } + } +}