100 Commits
1.1 ... travis

Author SHA1 Message Date
Alan Shreve
2c00a3fd8b Merge branch 'master' into travis 2013-10-25 18:13:13 +03:00
Alan Shreve
7a9f972030 add some more entries to the changelog 2013-10-25 18:12:56 +03:00
Alan Shreve
264fbe83fe bump version 2013-10-25 17:44:38 +03:00
Alan Shreve
3f8e4b8e0b fix a goroutine leak in ngrok's parsing of http transactions 2013-10-25 17:42:52 +03:00
Alan Shreve
5ddfa6232c fix a bug handling 'disabled' value for the inspection interface configuration 2013-10-25 13:06:51 +03:00
Alan Shreve
cda0e2c050 fix a bug handling 'disabled' value for the inspection interface configuration 2013-10-22 01:20:39 +03:00
Alan Shreve
69733b1e3c Merge branch 'master' into travis 2013-10-20 16:19:00 +03:00
Alan Shreve
92569be139 bump version 2013-10-20 15:46:57 +03:00
Alan Shreve
c72868cbf5 start cross-compiling to OS X on this branch as well 2013-10-20 15:39:25 +03:00
Alan Shreve
e58d35972d fix an issue where aggressive NATs or proxy connections pooled for too long could hang the tunneling of public connections 2013-10-15 17:27:45 +03:00
Alan Shreve
98902cff73 add support for specifying a remote tcp port 2013-10-13 14:32:23 +02:00
Alan Shreve
a269effc05 Revert "remove ip-based caching so that it's possible to reliably generate new urls"
This reverts commit f2009d7f52.
2013-10-13 14:26:31 +02:00
Alan Shreve
ed2abcaa95 support disabling the web inspection interface with 'disabled' 2013-10-13 14:04:52 +02:00
Alan Shreve
ea93073174 show error reporting instructions after crash dump 2013-10-08 12:16:05 +02:00
Alan Shreve
f2009d7f52 remove ip-based caching so that it's possible to reliably generate new urls 2013-10-08 12:13:26 +02:00
Alan Shreve
f956a32d64 improve connection logging 2013-10-08 12:04:29 +02:00
Alan Shreve
85723df89b improve metrics logging 2013-10-02 09:44:31 +02:00
Alan Shreve
1860b0a1e1 batch metric reporting to keen to improve performance at high request loads 2013-10-02 00:25:05 +02:00
Alan Shreve
f20d426690 fix developers guide debug command 2013-09-30 11:23:01 +02:00
Alan Shreve
d0c3f9cd03 stop using CloseRead() until I figure out linger/flush issues 2013-09-30 09:56:44 +02:00
Alan Shreve
148702929b switch back to Close() calls since CloseRead() seems to cause sockets not to flush their writes 2013-09-30 01:54:23 +02:00
Alan Shreve
3af3e39b20 Recover any errors while handling tunnel connections. Fix a race condition that could cause the server to send its initial auth message out of order or even on a closed channel and crash the server 2013-09-29 22:34:23 +02:00
Alan Shreve
ef9aa6251d safely shutdown connections by using TCPConn.CloseRead() so that they don't throw errors 2013-09-29 00:21:37 +02:00
Alan Shreve
4ce53025e1 go fmt 2013-09-29 00:20:22 +02:00
Alan Shreve
46dd79a949 fix a critical bug which could cause tunnels to freeze indefinitely and never clean up resources. fix a bug where a client trying to reconnect on an intermittent connection would be told that its tunnels were already registered by another user 2013-09-28 19:31:09 +02:00
Alan Shreve
d08b89020f update development documentation to reflect changes to the ngrok protocol 2013-09-28 11:58:59 +02:00
Alan Shreve
39bebc5e25 make note in developer guide that you must compile with Go1.1+ 2013-09-27 22:14:29 +02:00
Alan Shreve
5335f85202 Merge branch 'master' into travis 2013-09-27 21:27:56 +02:00
Alan Shreve
0134667055 bump version 2013-09-27 21:26:06 +02:00
Alan Shreve
9de09f3f1d fix a bug where your auth token couldn't be saved to a configuration file if no configuration file existed 2013-09-27 21:24:50 +02:00
Alan Shreve
0ca93d33aa Merge branch 'master' of github.com:inconshreveable/ngrok 2013-09-27 17:02:45 +02:00
Alan Shreve
0468760901 add comment around removing the workaround when the bug is fixed in termbox-go 2013-09-27 17:02:10 +02:00
Nick Presta
c47241cf3a Refactoring to support unicode. 2013-09-26 23:15:37 -04:00
Nick Presta
54b2056c12 Refactoring to remove cleverness. 2013-09-26 21:49:44 -04:00
Nick Presta
1d93622aec Adding myself to CONTRIBUTORS 2013-09-26 15:07:45 -04:00
inconshreveable
c7c9a2d07a Merge pull request #35 from NickPresta/fix-wrapping-web
Adding word wrapping to path for web dashboard.
2013-09-26 12:06:06 -07:00
Nick Presta
f136b8486e Adding word wrapping to path for web dashboard.
* Tested in latest versions of Chrome, Firefox, Safari.
2013-09-26 02:22:32 -04:00
Nick Presta
7fc854b620 Limiting the length of the request's path. 2013-09-26 01:29:07 -04:00
Alan Shreve
3d8f09ba08 fix up docs 2013-09-24 23:24:39 +02:00
Alan Shreve
ed5729bd63 Merge branch 'master' into travis 2013-09-22 17:40:17 +02:00
Alan Shreve
ce2b352574 bump version 2013-09-22 17:21:29 +02:00
Alan Shreve
179a76b8cf re-add support to write the configuration file when the server validates a new authtoken works 2013-09-22 17:21:05 +02:00
Alan Shreve
8d044e722c improve config validation, parse the tunnel name as a subdomain or hostname if neither is specified 2013-09-22 00:50:14 +03:00
Alan Shreve
5608df7ec5 parse old v0 configuration files for backwards-compat 2013-09-21 22:28:14 +03:00
Alan Shreve
7bf4547911 go fmt 2013-09-21 21:25:23 +03:00
Alan Shreve
7b25fe80e0 connect through to an HTTPS endpoint in the hopes that it will eliminate possible errors from proxies trying to parse the http request 2013-09-21 21:24:43 +03:00
Alan Shreve
97fcb417ac rename proxy_url configuration variable 2013-09-20 19:41:50 +03:00
Alan Shreve
22537c0136 add bug reporting instructions on crash 2013-09-20 19:41:31 +03:00
Alan Shreve
11dfbc7045 update self-hosting document 2013-09-20 19:40:52 +03:00
Alan Shreve
824a4b10ab add support for a yaml-based ngrok configuration file. add full support for running multiple configured tunnels from the configuration file. modify CLI to support ngrok commands. remove snakeoil CA from release builds of ngrok client. add support for dialing to an HTTP proxy via HTTPS. Use http_proxy environment variable when none is specified. ngrokd now honors the new protocol and proplery populates ReqId in NewTunnel messages. ngrokd TLS configuration options are now command line switches instead of environment variables. 2013-09-20 16:12:00 +03:00
Alan Shreve
13be54d4e7 make sure every new tunnel connection and every new http connection has a read/write timeouts until handled by a tunnel. fix a bug where a slow tunnel or proxy connection could block all others. fix a bug where sending a non-expected valid ngrok protocol message over a new connection could leak a connection 2013-09-16 13:12:44 +03:00
Alan Shreve
15e017d285 add support for proxy authentication 2013-09-13 21:28:56 +03:00
Alan Shreve
00ae5d77db Merge branch 'master' into travis 2013-09-06 22:59:15 +03:00
Alan Shreve
a911f7fb65 Merge branch 'master' into travis 2013-09-04 18:43:36 +03:00
Alan Shreve
a9f659216e Merge branch 'master' into travis 2013-08-17 21:54:29 +02:00
Alan Shreve
264ac352f7 Merge branch 'master' into travis 2013-08-17 21:25:50 +02:00
Alan Shreve
d0b9161993 Merge branch 'master' into travis 2013-08-17 12:19:41 +02:00
Alan Shreve
fd394cb336 Merge branch 'master' into travis 2013-08-17 12:10:33 +02:00
Alan Shreve
8f6637c784 Merge branch 'master' into travis 2013-08-17 11:17:21 +02:00
Alan Shreve
eb60b72aa4 Merge branch 'master' into travis 2013-08-17 11:16:33 +02:00
Alan Shreve
63c70c4996 don't cross-compile osx anymore 2013-08-16 01:58:59 +02:00
Alan Shreve
20364f6de0 Merge branch 'master' into travis 2013-08-15 13:10:19 +02:00
Alan Shreve
d914e26055 Merge branch 'master' into travis 2013-08-15 12:42:28 +02:00
Alan Shreve
f5a24b30e8 Merge branch 'master' into travis 2013-08-15 12:41:48 +02:00
Alan Shreve
bfbfdd9ee9 cross-compile for darwin/amd64 2013-08-15 12:41:42 +02:00
Alan Shreve
b4082e7f89 fix merge conflicts 2013-07-30 16:20:21 +02:00
Alan Shreve
151433e2fd merge in master 2013-07-30 16:08:18 +02:00
Alan Shreve
eef7b6b755 merge in branch master 2013-07-30 15:27:15 +02:00
Alan Shreve
816f5e8882 Merge branch 'master' into travis 2013-07-28 00:44:43 +02:00
Alan Shreve
8bfebdc277 remove unecessary package step 2013-07-23 15:40:36 +01:00
Alan Shreve
4e20bd9db8 fix build script 2013-07-18 19:08:07 +01:00
Alan Shreve
d2ec1ec080 fix windows build with exe extension 2013-07-18 18:24:55 +01:00
Alan Shreve
8017c0653b figure out why windows isn't building 2013-07-18 18:11:56 +01:00
Alan Shreve
167cf25dae remove debugging prints 2013-07-18 02:46:32 +01:00
Alan Shreve
9f8bd8adc3 make sure we run the verison command in native platform 2013-07-18 02:35:52 +01:00
Alan Shreve
4db6e7143d fix how we pass in environment variables 2013-07-18 02:29:08 +01:00
Alan Shreve
65411b54bd more travis debugging 2013-07-18 02:05:55 +01:00
Alan Shreve
213eb595a6 touch to get travis to build 2013-07-18 01:58:47 +01:00
Alan Shreve
160bf79cb5 pass env variables to travis 2013-07-18 01:45:00 +01:00
Alan Shreve
95ffd273f2 try to debug travis build problems 2013-07-18 01:09:47 +01:00
Alan Shreve
063389d36b new travis upload strategy 2013-07-18 00:56:32 +01:00
Alan Shreve
514c16c91f fix host target 2013-07-17 01:13:09 +01:00
Alan Shreve
f6a3742a74 set env vars separately 2013-07-17 00:59:10 +01:00
Alan Shreve
d09037b0ff run after_success explicitly 2013-07-17 00:50:19 +01:00
Alan Shreve
0c970983e2 one more attempt at fixing after_success 2013-07-17 00:43:32 +01:00
Alan Shreve
a72e0fd7c7 fix after_success 2013-07-17 00:27:56 +01:00
Alan Shreve
41865cb20d fix after_success 2013-07-17 00:21:47 +01:00
Alan Shreve
66a919e63b always install go-bindata for the current architecture 2013-07-17 00:07:51 +01:00
Alan Shreve
569013e691 fix for bootstrap 2013-07-16 23:49:16 +01:00
Alan Shreve
bf6ce69203 bootstrap for cross-compile 2013-07-16 23:46:35 +01:00
Alan Shreve
030b436d62 find make.bash 2013-07-16 23:37:44 +01:00
Alan Shreve
5b27861135 add cross-compilation targets 2013-07-16 23:30:03 +01:00
Alan Shreve
aa3fa9a08b continue trying to get travis to build 2013-07-16 23:26:19 +01:00
Alan Shreve
53a940ec18 try to get travis to build again 2013-07-16 23:16:42 +01:00
Alan Shreve
e5840c1e5d fix travis branch specifier 2013-07-16 23:10:11 +01:00
Alan Shreve
f1766e5884 test travis configuration 2013-07-16 22:57:38 +01:00
Alan Shreve
404e70c1eb add travis upload step 2013-07-16 22:53:39 +01:00
Alan Shreve
421b365483 add cross-compilation for windows and linux/arm 2013-07-16 20:50:49 +01:00
Alan Shreve
c072be702c make sure go-bindata tool is installed 2013-07-16 20:34:01 +01:00
Alan Shreve
0f09e5feae add travis integration 2013-07-16 20:28:33 +01:00
39 changed files with 1223 additions and 543 deletions

1
.gitignore vendored
View File

@@ -4,5 +4,6 @@ pkg/
src/code.google.com
src/github.com
src/bitbucket.org
src/launchpad.net
src/ngrok/client/assets/assets_release.go
src/ngrok/server/assets/assets_release.go

View File

@@ -1,4 +1,22 @@
script: make release-all
install: true
after_success:
- sudo pip install boto
- ls -lr bin/*
- NGROK="bin/${GOOS}_${GOARCH}/ngrok" NGROKD="bin/${GOOS}_${GOARCH}/ngrokd" VERSION=$(GOOS="" GOARCH="" GOPATH=$(pwd) go run build/version.go) python build/travis.py
before_script:
- pushd /home/travis/.gvm/gos/go1.1.1/src
- ./make.bash
- popd
env:
global:
- secure: Fd5zHi58jx8lsPDv4tkRFzXSY0KnPJZuZ+LvnRcpX4+3xJsuZU6moOfrOcGqDOm7/SqZRVZRKZapE772+8sXNKPmwSXHRsZEsUgqxdehFzlVP4PQN5efOdI/quO2ibwVpZ6Idze5pelZburALd7/VbfHCTB/0P0WDMNvfHuFPQg=
- secure: Yg+y996B4S7zHXO8j6JrRbgMf6yilHGWv6I+7oZf02d8IHYtAb6A9DveX/q+v24O8Q9WzXRU4ZIaG5nJksVwb19qcy4vpaUbvx00COi8tg0l9hdKIotrzAs+i8q5h0xUifE1iuqYJw39vYU/9vC/727GTvOfRpBCr+edcpc9uy0=
- secure: CMF8ZfcoZK8AhFrmo6ljwP4ulYOSI9Pftp02rfNcM7LYPzU5HxFmcR7M4+3Qt0HAWxYrReozNQZYrV0s5jqIO0H4LZeHxgi7EHTam1BelhmHxL6VBMoR1mTi53bErWNhpoc0qyCnWMpnzNMObwtBVm9vjacm4NJMWqQjte/0Kdw=
matrix:
- GOOS=linux GOARCH=arm
- GOOS=linux GOARCH=386
- GOOS=windows GOARCH=386
- GOOS=darwin GOARCH=amd64
go:
- 1.1

View File

@@ -3,3 +3,4 @@ Alan Shreve (inconshreveable)
Kyle Conroy (kyleconroy)
Caleb Spare (cespare)
Stephen Huenneke (skastel)
Nick Presta (nickpresta)

View File

@@ -31,11 +31,4 @@ are only available by creating an account on ngrok.com. If you need them, [creat
## Developing on ngrok
### Compiling ngrok
git clone git@github.com:inconshreveable/ngrok.git
cd ngrok && make
bin/ngrok [LOCAL PORT]
### Further Reading
[ngrok developer's guide](docs/DEVELOPMENT.md)

View File

@@ -23,7 +23,14 @@
background-color: #ff9999;
background-color: #000000;
color:white;
}
.path {
width: 300px;
}
.wrapped {
word-wrap: break-word;
word-break: break-word;
overflow: hidden;
}
</style>
</head>
@@ -62,7 +69,7 @@
<h4>All Requests</h4>
<table class="table txn-selector">
<tr ng-controller="TxnNavItem" ng-class="{'selected':isActive()}" ng-repeat="txn in txns" ng-click="makeActive()">
<td>{{ txn.Req.MethodPath }}</td>
<td class="wrapped"><div class="path">{{ txn.Req.MethodPath }}</div></td>
<td>{{ txn.Resp.Status }}</td>
<td><span class="pull-right">{{ txn.Duration }}</span></td>
</tr>
@@ -86,7 +93,7 @@
</div>
<hr />
<div ng-show="!!Req" ng-controller="HttpRequest">
<h3>{{ Req.MethodPath }}</h3>
<h3 class="wrapped">{{ Req.MethodPath }}</h3>
<div onbtnclick="replay()" btn="Replay" tabs="Summary,Headers,Raw,Binary">
</div>

22
build/travis.py Executable file
View File

@@ -0,0 +1,22 @@
#! /usr/bin/env python
import os, os.path, boto.s3.connection
access_key = os.getenv("AWS_ACCESS_KEY")
secret_key = os.getenv("AWS_SECRET_KEY")
bucket = os.getenv("BUCKET")
version = os.getenv("VERSION")
goos = os.getenv("GOOS")
s3 = boto.s3.connection.S3Connection(access_key, secret_key)
bucket = s3.get_bucket(bucket)
for envpath in ["NGROK", "NGROKD"]:
file_path = os.getenv(envpath)
if goos == "windows":
file_path += ".exe"
dir_path, name = os.path.split(file_path)
_, platform = os.path.split(dir_path)
key_name = "%s/%s/%s" % (platform, version, name)
key = bucket.new_key(key_name)
key.set_contents_from_filename(file_path)

10
build/version.go Normal file
View File

@@ -0,0 +1,10 @@
package main
import (
"fmt"
"ngrok/version"
)
func main() {
fmt.Print(version.MajorMinor())
}

View File

@@ -1,4 +1,24 @@
# Changelog
## 1.6 - 10/25/2013
- BUGFIX: Fixed a goroutine/memory leak in ngrok/proto's parsing of http traffic
- IMPROVEMENT: The web inspection API can now be disabled again by setting inspect_addr: disabled in the config file
## 1.5 - 10/20/2013
- FEATURE: Added support a "remote_port" configuration parameter that lets you request a specific remote port for TCP tunnels
- IMPROVEMENT: Upload instructions on crash reports are displayed after the dump where it is more likely to be seen
- IMPROVEMENT: Improvements to ngrok's logging for easier debugging
- IMPROVEMENT: Batch metric reporting to Keen to not be limited by the speed of their API at high request loads
- IMPROVEMENT: Added additional safety to ensure the server doesn't crash on panics()
- BUGFIX: Fixed an issue with prefetching tunnel connections that could hang tunnel connections when behind an aggresive NAT
- BUGFIX: Fixed a race condition where ngrokd could send back a different message instead of AuthResp first
- BUGFIX: Fixed an issue where under some circumstances, reconnecting would fail and tell the client the tunnels were still in use
- BUGFIX: Fixed an issue where a race-condition with handling pings could cause a tunnel to hang forever and stop handling requests
## 1.4 - 09/27/2013
- BUGFIX: Fixed an issue where long URL paths were not truncated in the terminal UI
- BUGFIX: Fixed an issue where long URL paths ruined the web UI's formatting
- BUGFIX: Fixed an issue where authtokens would not be remembered if an existing configuration file didn't exist
## 0.23 - 09/06/2013
- BUGFIX: Fixed a bug which caused some important HTTP headers to be omitted from request introspection and replay

View File

@@ -16,6 +16,8 @@ There are Makefile targets for compiling just the client or server.
make client
make server
**NB: You must compile with Go 1.1+!**
### Compiling release versions
Both the client and the server contain static asset files.
These include TLS/SSL certificates and the html/css/js for the client's web interface.
@@ -36,15 +38,25 @@ The strategy I use for developing on ngrok is to do the following:
Add the following lines to /etc/hosts:
127.0.0.1 ngrok.me
127.0.0.1 tunnel.ngrok.me
127.0.0.1 test.ngrok.me
Run ngrokd with the following options:
./bin/ngrokd -domain ngrok.me
Run ngrok with the following options
Create an ngrok configuration file, "debug.yml" with the following contents:
./bin/ngrok -server=ngrok.me:4443 -subdomain=tunnel -log=ngrok.log 8080
server_addr: ngrok.me:4443
tunnels:
test:
proto:
http: 8080
Then run ngrok with either of these commands:
./bin/ngrok -config=debug.yml -log=ngrok.log start test
./bin/ngrok -config=debug.yml -log=ngrok.log -subdomain=test 8080
This will get you setup with an ngrok client talking to an ngrok server all locally under your control. Happy hacking!
@@ -52,18 +64,23 @@ This will get you setup with an ngrok client talking to an ngrok server all loca
## Network protocol and tunneling
At a high level, ngrok's tunneling works as follows:
### Setup
### Connection Setup and Authentication
1. The client initiates a long-lived TCP connection to the server over which they will pass JSON instruction messages. This connection is called the *Control Connection*.
1. After the connection is established, the client sends a registration message.
1. The server registers the tunnel and then sends a registration ACK message with information about the created tunnel.
1. After the connection is established, the client sends an *Auth* message with authentication and version information.
1. The server validates the client's *Auth* message and sends an *AuthResp* message indicating either success or failure.
### Tunnel creation
1. The client may then ask the server to create tunnels for it by sending *ReqTunnel* messages.
1. When the server receives a *ReqTunnel* message, it will send 1 or more *NewTunnel* messages that indicate successful tunnel creation or indicate failure.
### Tunneling connections
1. When the server receives a new public connection, it locates the approriate tunnel by examing the HTTP host header (or the port number for TCP tunnels). This connection from the public internet is called a *Public Connection*.
1. The server sends a proxy request message to the client over the control connection.
1. When the server receives a new public connection, it locates the approriate tunnel by examining the HTTP host header (or the port number for TCP tunnels). This connection from the public internet is called a *Public Connection*.
1. The server sends a *ReqProxy* message to the client over the control connection.
1. The client initiates a new TCP connection to the server called a *Proxy Connection*.
1. The client sends a proxy registration message over the proxy connection so the server can associate it to the public connection that caused the request for the proxy connection to be initiated.
1. The client sends a *RegProxy* message over the proxy connection so the server can associate it to a control connection (and thus the tunnels it's responsible for).
1. The server sends a *StartProxy* message over the proxy connection with metadata information about the connection (the client IP and name of the tunnel).
1. The server begins copying the traffic byte-for-byte from the public connection to the proxy connection and vice-versa.
1. The client opens a connection to the local port that was specified at startup. This is called the *Private Connection*.
1. The client opens a connection to the local address configured for that tunnel. This is called the *Private Connection*.
1. The client begins copying the traffic byte-for-byte from the proxied connection to the private connection and vice-versa.
### Detecting dead tunnels
@@ -81,7 +98,7 @@ The message length is sent as a 64-bit little endian integer.
The definitions and shared protocol routines lives under _src/ngrok/msg_
#### src/ngrok/msg/msg.go
All of the different message types (Reg, PxyReq, Ping, etc) are defined here. This is a good place to go to understand exactly what messages are sent between the client and server.
All of the different message types (Auth, AuthResp, ReqTunnel, RegProxy, StartProxy, etc) are defined here and their fields documented. This is a good place to go to understand exactly what messages are sent between the client and server.
## ngrokd - the server
### Code

63
docs/SELFHOSTING.md Normal file
View File

@@ -0,0 +1,63 @@
# How to run your own ngrokd server
Running your own ngrok server is really easy! The instructions below will guide you along your way!
## 1. Get an SSL certificate
ngrok provides secure tunnels via TLS, so you'll need an SSL certificate. Assuming you want to create
tunnels on *.example.com, buy a wildcard SSL certificate for *.example.com.
## 2. Modify your DNS
You need to use the DNS management tools given to you by your provider to create an A
record which points *.example.com to the IP address of the server where you will run ngrokd.
## 3. Compile it
You can compile an ngrokd server with the following command:
make release-server
Make sure you compile it with the GOOS/GOARCH environment variables set to the platform of
your target server. Then copy the binary over to your server.
## 4. Run the server
You'll run the server with the following command.
./ngrokd -tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt" -domain="example.com"
### Specifying your TLS certificate and key
ngrok only makes TLS-encrypted connections. When you run ngrokd, you'll need to instruct it
where to find your TLS certificate and private key. Specify the paths with the following switches:
-tlsKey="/path/to/tls.key" -tlsCert="/path/to/tls.crt"
### Setting the server's domain
When you run your own ngrokd server, you need to tell ngrokd the domain it's running on so that it
knows what URLs to issue to clients.
-domain="example.com"
## 5. Configure the client
In order to connect with a client, you'll need to set two options in ngrok's configuration file.
The ngrok configuration file is a simple YAML file that is read from ~/.ngrok by default. You may specify
a custom configuration file path with the -config switch. Your config file must contain the following two
options.
server_addr: example.com:4443
trust_host_root_certs: true
Subsitute the address of your ngrokd server for "example.com:4443". The "trust_host_root_certs" parameter instructs
ngrok to trust the root certificates on your computer when establishing TLS connections to the server. By default, ngrok
only trusts the root certificate for ngrok.com.
## 6. Connect with a client
Then, just run ngrok as usual to connect securely to your own ngrokd server!
ngrok 80
## FAQ
#### Do I really need a wildcard certificate?
If you don't need to run https tunnels, then you don't need a wildcard cert.
#### I don't want to pay for an SSL certificate, can I use a self-signed one?
Yes, it's possible to use a self-signed certificate, but you'll need to recompile ngrok with your signing CA.

View File

@@ -1,64 +0,0 @@
package client
/*
Functions for reading and writing the auth token from the user's
home directory.
*/
import (
"io/ioutil"
"ngrok/log"
"os"
"os/user"
"path"
"sync"
)
var (
once sync.Once
currentAuthToken string
authTokenFile string
)
func initAuth() {
user, err := user.Current()
// user.Current() does not work on linux when cross compilling because
// it requires CGO; use os.Getenv("HOME") hack until we compile natively
homeDir := os.Getenv("HOME")
if err != nil {
log.Warn("Failed to get user's home directory: %s", err.Error())
} else {
homeDir = user.HomeDir
}
authTokenFile = path.Join(homeDir, ".ngrok")
log.Debug("Reading auth token from file %s", authTokenFile)
tokenBytes, err := ioutil.ReadFile(authTokenFile)
if err == nil {
currentAuthToken = string(tokenBytes)
} else {
log.Warn("Failed to read ~/.ngrok for auth token: %s", err.Error())
}
}
// Load the auth token from file
func LoadAuthToken() string {
once.Do(initAuth)
return currentAuthToken
}
// Save the auth token to file
func SaveAuthToken(token string) {
if token == "" || token == LoadAuthToken() || authTokenFile == "" {
return
}
perms := os.FileMode(0644)
err := ioutil.WriteFile(authTokenFile, []byte(token), perms)
if err != nil {
log.Warn("Failed to write auth token to file %s: %v", authTokenFile, err.Error())
}
}

View File

@@ -1,112 +1,72 @@
package client
import (
"errors"
"flag"
"fmt"
"net"
"ngrok/version"
"os"
"strconv"
)
var (
PORT_OUT_OF_RANGE error = errors.New("Port number must be between 1 and 65535")
)
const usage1 string = `Usage: %s [OPTIONS] <local port or address>
Options:
`
const usage2 string = `
Examples:
ngrok 80
ngrok -subdomain=example 8080
ngrok -proto=tcp 22
ngrok -hostname="example.com" -httpauth="user:password" 10.0.0.1
Advanced usage: ngrok [OPTIONS] <command> [command args] [...]
Commands:
ngrok start [tunnel] [...] Start tunnels by name from config file
ngrok help Print help
ngrok version Print ngrok version
Examples:
ngrok start www api blog pubsub
ngrok -log=stdout -config=ngrok.yml start ssh
ngrok version
`
type Options struct {
serverAddr string
proxyAddr string
httpAuth string
hostname string
localaddr string
protocol string
url string
subdomain string
webport int
logto string
authtoken string
config string
logto string
authtoken string
httpauth string
hostname string
protocol string
subdomain string
command string
args []string
}
func fail(msg string, args ...interface{}) {
fmt.Printf(msg+"\n", args...)
flag.PrintDefaults()
os.Exit(1)
}
func parsePort(portString string) (err error) {
var port int
if port, err = strconv.Atoi(portString); err != nil {
return err
func parseArgs() (opts *Options, err error) {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, usage1, os.Args[0])
flag.PrintDefaults()
fmt.Fprintf(os.Stderr, usage2)
}
if port < 1 || port > 65535 {
return PORT_OUT_OF_RANGE
}
config := flag.String(
"config",
"",
"Path to ngrok configuration file. (default: $HOME/.ngrok)")
return
}
logto := flag.String(
"log",
"none",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
// Local address could be a port of a host:port string
// we always return a host:port string from this function or fail
func parseLocalAddr() string {
if flag.NArg() == 0 {
fail("LOCAL not specified, specify a port number or host:port connection string")
}
if flag.NArg() > 1 {
fail("Only one LOCAL may be specified, not %d", flag.NArg())
}
addr := flag.Arg(0)
// try to parse as a port number
if err := parsePort(addr); err == nil {
return fmt.Sprintf("127.0.0.1:%s", addr)
} else if err == PORT_OUT_OF_RANGE {
fail("%s is not in the valid port range 1-65535")
}
// try to parse as a connection string
_, port, err := net.SplitHostPort(addr)
if err != nil {
fail("%v", err)
}
if parsePort(port) != nil {
fail("'%s' is not a valid port number (1-65535)", port)
}
return addr
}
func parseProtocol(proto string) string {
switch proto {
case "http", "https", "http+https", "tcp":
return proto
default:
fail("%s is not a valid protocol", proto)
}
panic("unreachable")
}
func parseArgs() *Options {
authtoken := flag.String(
"authtoken",
"",
"Authentication token for identifying a premium ngrok.com account")
"Authentication token for identifying an ngrok.com account")
serverAddr := flag.String(
"serverAddr",
"ngrokd.ngrok.com:443",
"Address of the remote ngrokd server")
proxyAddr := flag.String(
"proxyAddr",
"",
"The address of an http proxy to connect through (ex: proxy.example.org:3128)")
httpAuth := flag.String(
httpauth := flag.String(
"httpauth",
"",
"username:password HTTP basic auth creds protecting the public tunnel endpoint")
@@ -114,7 +74,7 @@ func parseArgs() *Options {
subdomain := flag.String(
"subdomain",
"",
"Request a custom subdomain from the ngrok server. (HTTP mode only)")
"Request a custom subdomain from the ngrok server. (HTTP only)")
hostname := flag.String(
"hostname",
@@ -126,38 +86,43 @@ func parseArgs() *Options {
"http+https",
"The protocol of the traffic over the tunnel {'http', 'https', 'tcp'} (default: 'http+https')")
webport := flag.Int(
"webport",
4040,
"The port on which the web interface is served, -1 to disable")
logto := flag.String(
"log",
"none",
"Write log messages to this file. 'stdout' and 'none' have special meanings")
v := flag.Bool(
"version",
false,
"Print ngrok version and exit")
flag.Parse()
if *v {
fmt.Println(version.MajorMinor())
os.Exit(0)
opts = &Options{
config: *config,
logto: *logto,
httpauth: *httpauth,
subdomain: *subdomain,
protocol: *protocol,
authtoken: *authtoken,
hostname: *hostname,
command: flag.Arg(0),
}
return &Options{
serverAddr: *serverAddr,
proxyAddr: *proxyAddr,
httpAuth: *httpAuth,
subdomain: *subdomain,
localaddr: parseLocalAddr(),
protocol: parseProtocol(*protocol),
webport: *webport,
logto: *logto,
authtoken: *authtoken,
hostname: *hostname,
switch opts.command {
case "start":
opts.args = flag.Args()[1:]
case "version":
fmt.Println(version.MajorMinor())
os.Exit(0)
case "help":
flag.Usage()
os.Exit(0)
case "":
err = fmt.Errorf("You must specify a local port to tunnel or an ngrok command.")
return
default:
if len(flag.Args()) > 1 {
err = fmt.Errorf("You may only specify one port to tunnel to on the command line, got %d: %v",
len(flag.Args()),
flag.Args())
return
}
opts.command = "default"
opts.args = flag.Args()
}
return
}

266
src/ngrok/client/config.go Normal file
View File

@@ -0,0 +1,266 @@
package client
import (
"fmt"
"io/ioutil"
"launchpad.net/goyaml"
"net"
"net/url"
"ngrok/log"
"os"
"os/user"
"path"
"regexp"
"strconv"
"strings"
)
type Configuration struct {
HttpProxy string `yaml:"http_proxy,omitempty"`
ServerAddr string `yaml:"server_addr,omitempty"`
InspectAddr string `yaml:"inspect_addr,omitempty"`
TrustHostRootCerts bool `yaml:"trust_host_root_certs,omitempty"`
AuthToken string `yaml:"auth_token,omitempty"`
Tunnels map[string]*TunnelConfiguration `yaml:"tunnels,omitempty"`
LogTo string `yaml:"-"`
Path string `yaml:"-"`
}
type TunnelConfiguration struct {
Subdomain string `yaml:"subdomain,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
Protocols map[string]string `yaml:"proto,omitempty"`
HttpAuth string `yaml:"auth,omitempty"`
RemotePort uint16 `yaml:"remote_port,omitempty"`
}
func LoadConfiguration(opts *Options) (config *Configuration, err error) {
configPath := opts.config
if configPath == "" {
configPath = defaultPath()
}
log.Info("Reading configuration file %s", configPath)
configBuf, err := ioutil.ReadFile(configPath)
if err != nil {
// failure to read a configuration file is only a fatal error if
// the user specified one explicitly
if opts.config != "" {
err = fmt.Errorf("Failed to read configuration file %s: %v", configPath, err)
return
}
}
// deserialize/parse the config
config = new(Configuration)
if err = goyaml.Unmarshal(configBuf, &config); err != nil {
err = fmt.Errorf("Error parsing configuration file %s: %v", configPath, err)
return
}
// try to parse the old .ngrok format for backwards compatibility
matched := false
content := strings.TrimSpace(string(configBuf))
if matched, err = regexp.MatchString("^[0-9a-zA-Z_\\-!]+$", content); err != nil {
return
} else if matched {
config = &Configuration{AuthToken: content}
}
// set configuration defaults
if config.ServerAddr == "" {
config.ServerAddr = defaultServerAddr
}
if config.InspectAddr == "" {
config.InspectAddr = "127.0.0.1:4040"
}
if config.HttpProxy == "" {
config.HttpProxy = os.Getenv("http_proxy")
}
// validate and normalize configuration
if config.InspectAddr != "disabled" {
if config.InspectAddr, err = normalizeAddress(config.InspectAddr, "inspect_addr"); err != nil {
return
}
}
if config.ServerAddr, err = normalizeAddress(config.ServerAddr, "server_addr"); err != nil {
return
}
if config.HttpProxy != "" {
var proxyUrl *url.URL
if proxyUrl, err = url.Parse(config.HttpProxy); err != nil {
return
} else {
if proxyUrl.Scheme != "http" && proxyUrl.Scheme != "https" {
err = fmt.Errorf("Proxy url scheme must be 'http' or 'https', got %v", proxyUrl.Scheme)
return
}
}
}
for name, t := range config.Tunnels {
if t == nil || t.Protocols == nil || len(t.Protocols) == 0 {
err = fmt.Errorf("Tunnel %s does not specify any protocols to tunnel.", name)
return
}
for k, addr := range t.Protocols {
tunnelName := fmt.Sprintf("for tunnel %s[%s]", name, k)
if t.Protocols[k], err = normalizeAddress(addr, tunnelName); err != nil {
return
}
if err = validateProtocol(k, tunnelName); err != nil {
return
}
}
// use the name of the tunnel as the subdomain if none is specified
if t.Hostname == "" && t.Subdomain == "" {
// XXX: a crude heuristic, really we should be checking if the last part
// is a TLD
if len(strings.Split(name, ".")) > 1 {
t.Hostname = name
} else {
t.Subdomain = name
}
}
}
// override configuration with command-line options
config.LogTo = opts.logto
config.Path = configPath
if opts.authtoken != "" {
config.AuthToken = opts.authtoken
}
switch opts.command {
// start a single tunnel, the default, simple ngrok behavior
case "default":
config.Tunnels = make(map[string]*TunnelConfiguration)
config.Tunnels["default"] = &TunnelConfiguration{
Subdomain: opts.subdomain,
Hostname: opts.hostname,
HttpAuth: opts.httpauth,
Protocols: make(map[string]string),
}
for _, proto := range strings.Split(opts.protocol, "+") {
if err = validateProtocol(proto, "default"); err != nil {
return
}
if config.Tunnels["default"].Protocols[proto], err = normalizeAddress(opts.args[0], ""); err != nil {
return
}
}
// start tunnels
case "start":
if len(opts.args) == 0 {
err = fmt.Errorf("You must specify at least one tunnel to start")
return
}
requestedTunnels := make(map[string]bool)
for _, arg := range opts.args {
requestedTunnels[arg] = true
if _, ok := config.Tunnels[arg]; !ok {
err = fmt.Errorf("Requested to start tunnel %s which is not defined in the config file.", arg)
return
}
}
for name, _ := range config.Tunnels {
if !requestedTunnels[name] {
delete(config.Tunnels, name)
}
}
default:
err = fmt.Errorf("Unknown command: %s", opts.command)
return
}
return
}
func defaultPath() string {
user, err := user.Current()
// user.Current() does not work on linux when cross compilling because
// it requires CGO; use os.Getenv("HOME") hack until we compile natively
homeDir := os.Getenv("HOME")
if err != nil {
log.Warn("Failed to get user's home directory: %s. Using $HOME: %s", err.Error(), homeDir)
} else {
homeDir = user.HomeDir
}
return path.Join(homeDir, ".ngrok")
}
func normalizeAddress(addr string, propName string) (string, error) {
// normalize port to address
if _, err := strconv.Atoi(addr); err == nil {
addr = ":" + addr
}
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
return "", fmt.Errorf("Invalid address %s '%s': %s", propName, addr, err.Error())
}
if tcpAddr.IP == nil {
tcpAddr.IP = net.ParseIP("127.0.0.1")
}
return tcpAddr.String(), nil
}
func validateProtocol(proto, propName string) (err error) {
switch proto {
case "http", "https", "http+https", "tcp":
default:
err = fmt.Errorf("Invalid protocol for %s: %s", propName, proto)
}
return
}
func SaveAuthToken(configPath, authtoken string) (err error) {
// empty configuration by default for the case that we can't read it
c := new(Configuration)
// read the configuration
oldConfigBytes, err := ioutil.ReadFile(configPath)
if err == nil {
// unmarshal if we successfully read the configuration file
if err = goyaml.Unmarshal(oldConfigBytes, c); err != nil {
return
}
}
// no need to save, the authtoken is already the correct value
if c.AuthToken == authtoken {
return
}
// update auth token
c.AuthToken = authtoken
// rewrite configuration
newConfigBytes, err := goyaml.Marshal(c)
if err != nil {
return
}
err = ioutil.WriteFile(configPath, newConfigBytes, 0600)
return
}

View File

@@ -6,7 +6,6 @@ import (
"ngrok/client/views/term"
"ngrok/client/views/web"
"ngrok/log"
"ngrok/msg"
"ngrok/proto"
"ngrok/util"
"sync"
@@ -48,7 +47,7 @@ type Controller struct {
state chan mvc.State
// options
opts *Options
config *Configuration
}
// public interface
@@ -127,29 +126,29 @@ func (ctl *Controller) addView(v mvc.View) {
ctl.views = append(ctl.views, v)
}
func (ctl *Controller) GetWebViewPort() int {
return ctl.opts.webport
func (ctl *Controller) GetWebInspectAddr() string {
return ctl.config.InspectAddr
}
func (ctl *Controller) Run(opts *Options) {
// Save the options
ctl.opts = opts
func (ctl *Controller) Run(config *Configuration) {
// Save the configuration
ctl.config = config
// init the model
model := newClientModel(ctl)
model := newClientModel(config, ctl)
ctl.model = model
var state mvc.State = model
// init web ui
var webView *web.WebView
if opts.webport != -1 {
webView = web.NewWebView(ctl, opts.webport)
if config.InspectAddr != "disabled" {
webView = web.NewWebView(ctl, config.InspectAddr)
ctl.addView(webView)
}
// init term ui
var termView *term.TermView
if opts.logto != "stdout" {
if config.LogTo != "stdout" {
termView = term.NewTermView(ctl)
ctl.addView(termView)
}
@@ -168,16 +167,8 @@ func (ctl *Controller) Run(opts *Options) {
}
}
ctl.Go(func() { autoUpdate(state, opts.authtoken) })
reqTunnel := &msg.ReqTunnel{
Protocol: opts.protocol,
Hostname: opts.hostname,
Subdomain: opts.subdomain,
HttpAuth: opts.httpAuth,
}
ctl.Go(func() { ctl.model.Run(opts.serverAddr, opts.proxyAddr, opts.authtoken, ctl, reqTunnel, opts.localaddr) })
ctl.Go(func() { autoUpdate(state, config.AuthToken) })
ctl.Go(ctl.model.Run)
updates := ctl.updates.Reg()
defer ctl.updates.UnReg(updates)

View File

@@ -0,0 +1,7 @@
// +build !release
package client
var (
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt", "assets/client/tls/snakeoilca.crt"}
)

View File

@@ -1,29 +1,38 @@
package client
import (
"fmt"
"math/rand"
"ngrok/log"
"ngrok/util"
"os"
)
func Main() {
// parse options
opts := parseArgs()
opts, err := parseArgs()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// set up logging
log.LogTo(opts.logto)
// set up auth token
if opts.authtoken == "" {
opts.authtoken = LoadAuthToken()
// read configuration file
config, err := LoadConfiguration(opts)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
// seed random number generator
seed, err := util.RandomSeed()
if err != nil {
log.Error("Couldn't securely seed the random number generator!")
fmt.Printf("Couldn't securely seed the random number generator!")
os.Exit(1)
}
rand.Seed(seed)
NewController().Run(opts)
NewController().Run(config)
}

View File

@@ -1,6 +1,7 @@
package client
import (
"crypto/tls"
"fmt"
metrics "github.com/inconshreveable/go-metrics"
"io/ioutil"
@@ -10,13 +11,16 @@ import (
"ngrok/log"
"ngrok/msg"
"ngrok/proto"
"ngrok/util"
"ngrok/version"
"runtime"
"strings"
"sync/atomic"
"time"
)
const (
defaultServerAddr = "ngrokd.ngrok.com:443"
pingInterval = 20 * time.Second
maxPongLatency = 15 * time.Second
updateCheckInterval = 6 * time.Hour
@@ -41,20 +45,43 @@ type ClientModel struct {
protocols []proto.Protocol
ctl mvc.Controller
serverAddr string
proxyAddr string
proxyUrl string
authToken string
tlsConfig *tls.Config
tunnelConfig map[string]*TunnelConfiguration
configPath string
}
func newClientModel(ctl mvc.Controller) *ClientModel {
func newClientModel(config *Configuration, ctl mvc.Controller) *ClientModel {
protoMap := make(map[string]proto.Protocol)
protoMap["http"] = proto.NewHttp()
protoMap["https"] = protoMap["http"]
protoMap["tcp"] = proto.NewTcp()
protocols := []proto.Protocol{protoMap["http"], protoMap["tcp"]}
// configure TLS
var tlsConfig *tls.Config
if config.TrustHostRootCerts {
tlsConfig = &tls.Config{}
} else {
var err error
if tlsConfig, err = LoadTLSConfig(rootCrtPaths); err != nil {
panic(err)
}
}
return &ClientModel{
Logger: log.NewPrefixLogger("client"),
// server address
serverAddr: config.ServerAddr,
// proxy address
proxyUrl: config.HttpProxy,
// auth token
authToken: config.AuthToken,
// connection status
connStatus: mvc.ConnConnecting,
@@ -75,6 +102,15 @@ func newClientModel(ctl mvc.Controller) *ClientModel {
// controller
ctl: ctl,
// tls configuration
tlsConfig: tlsConfig,
// tunnel configuration
tunnelConfig: config.Tunnels,
// config path
configPath: config.Path,
}
}
@@ -130,22 +166,16 @@ func (c *ClientModel) update() {
c.ctl.Update(c)
}
func (c *ClientModel) Run(serverAddr, proxyAddr, authToken string, ctl mvc.Controller, reqTunnel *msg.ReqTunnel, localaddr string) {
c.serverAddr = serverAddr
c.proxyAddr = proxyAddr
c.authToken = authToken
c.ctl = ctl
c.reconnectingControl(reqTunnel, localaddr)
}
func (c *ClientModel) reconnectingControl(reqTunnel *msg.ReqTunnel, localaddr string) {
func (c *ClientModel) Run() {
// how long we should wait before we reconnect
maxWait := 30 * time.Second
wait := 1 * time.Second
for {
c.control(reqTunnel, localaddr)
// run the control channel
c.control()
// control oonly returns when a failure has occurred, so we're going to try to reconnect
if c.connStatus == mvc.ConnOnline {
wait = 1 * time.Second
}
@@ -161,7 +191,7 @@ func (c *ClientModel) reconnectingControl(reqTunnel *msg.ReqTunnel, localaddr st
}
// Establishes and manages a tunnel control connection with the server
func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
func (c *ClientModel) control() {
defer func() {
if r := recover(); r != nil {
log.Error("control recovering from failure %v", r)
@@ -173,11 +203,11 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
ctlConn conn.Conn
err error
)
if c.proxyAddr == "" {
if c.proxyUrl == "" {
// simple non-proxied case, just connect to the server
ctlConn, err = conn.Dial(c.serverAddr, "ctl", tlsConfig)
ctlConn, err = conn.Dial(c.serverAddr, "ctl", c.tlsConfig)
} else {
ctlConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "ctl", tlsConfig)
ctlConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "ctl", c.tlsConfig)
}
if err != nil {
panic(err)
@@ -214,11 +244,36 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
c.serverVersion = authResp.MmVersion
c.Info("Authenticated with server, client id: %v", c.id)
c.update()
SaveAuthToken(c.authToken)
if err = SaveAuthToken(c.configPath, c.authToken); err != nil {
c.Error("Failed to save auth token: %v", err)
}
// register the tunnel
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
panic(err)
// request tunnels
reqIdToTunnelConfig := make(map[string]*TunnelConfiguration)
for _, config := range c.tunnelConfig {
// create the protocol list to ask for
var protocols []string
for proto, _ := range config.Protocols {
protocols = append(protocols, proto)
}
reqTunnel := &msg.ReqTunnel{
ReqId: util.RandId(8),
Protocol: strings.Join(protocols, "+"),
Hostname: config.Hostname,
Subdomain: config.Subdomain,
HttpAuth: config.HttpAuth,
RemotePort: config.RemotePort,
}
// send the tunnel request
if err = msg.WriteMsg(ctlConn, reqTunnel); err != nil {
panic(err)
}
// save request id association so we know which local address
// to proxy to later
reqIdToTunnelConfig[reqTunnel.ReqId] = config
}
// start the heartbeat
@@ -249,7 +304,7 @@ func (c *ClientModel) control(reqTunnel *msg.ReqTunnel, localaddr string) {
tunnel := mvc.Tunnel{
PublicUrl: m.Url,
LocalAddr: localaddr,
LocalAddr: reqIdToTunnelConfig[m.ReqId].Protocols[m.Protocol],
Protocol: c.protoMap[m.Protocol],
}
@@ -271,10 +326,10 @@ func (c *ClientModel) proxy() {
err error
)
if c.proxyAddr == "" {
remoteConn, err = conn.Dial(c.serverAddr, "pxy", tlsConfig)
if c.proxyUrl == "" {
remoteConn, err = conn.Dial(c.serverAddr, "pxy", c.tlsConfig)
} else {
remoteConn, err = conn.DialHttpProxy(c.proxyAddr, c.serverAddr, "pxy", tlsConfig)
remoteConn, err = conn.DialHttpProxy(c.proxyUrl, c.serverAddr, "pxy", c.tlsConfig)
}
if err != nil {
@@ -285,20 +340,20 @@ func (c *ClientModel) proxy() {
err = msg.WriteMsg(remoteConn, &msg.RegProxy{ClientId: c.id})
if err != nil {
log.Error("Failed to write RegProxy: %v", err)
remoteConn.Error("Failed to write RegProxy: %v", err)
return
}
// wait for the server to ack our register
var startPxy msg.StartProxy
if err = msg.ReadMsgInto(remoteConn, &startPxy); err != nil {
log.Error("Server failed to write StartProxy: %v", err)
remoteConn.Error("Server failed to write StartProxy: %v", err)
return
}
tunnel, ok := c.tunnels[startPxy.Url]
if !ok {
c.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
remoteConn.Error("Couldn't find tunnel for proxy: %s", startPxy.Url)
return
}

View File

@@ -23,6 +23,6 @@ type Controller interface {
// safe wrapper for running go-routines
Go(fn func())
// the port where the web interface is running
GetWebViewPort() int
// the address where the web inspection interface is running
GetWebInspectAddr() string
}

View File

@@ -1,11 +1,7 @@
package mvc
import (
"ngrok/msg"
)
type Model interface {
Run(serverAddr, proxyAddr, authToken string, ctl Controller, reqTunnel *msg.ReqTunnel, localaddr string)
Run()
Shutdown()

View File

@@ -0,0 +1,7 @@
// +build release
package client
var (
rootCrtPaths = []string{"assets/client/tls/ngrokroot.crt"}
)

View File

@@ -4,42 +4,34 @@ import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"ngrok/client/assets"
)
var (
tlsConfig *tls.Config
)
func init() {
func LoadTLSConfig(rootCertPaths []string) (*tls.Config, error) {
pool := x509.NewCertPool()
ngrokRootCrt, err := assets.ReadAsset("assets/client/tls/ngrokroot.crt")
if err != nil {
panic(err)
}
for _, certPath := range rootCertPaths {
rootCrt, err := assets.ReadAsset(certPath)
if err != nil {
return nil, err
}
snakeoilCaCrt, err := assets.ReadAsset("assets/client/tls/snakeoilca.crt")
if err != nil {
panic(err)
}
for _, b := range [][]byte{ngrokRootCrt, snakeoilCaCrt} {
pemBlock, _ := pem.Decode(b)
pemBlock, _ := pem.Decode(rootCrt)
if pemBlock == nil {
panic("Bad PEM data")
return nil, fmt.Errorf("Bad PEM data")
}
certs, err := x509.ParseCertificates(pemBlock.Bytes)
if err != nil {
panic(err)
return nil, err
}
pool.AddCert(certs[0])
}
tlsConfig = &tls.Config{
return &tls.Config{
RootCAs: pool,
ServerName: "tls.ngrok.com",
}
ServerName: "ngrokd.ngrok.com",
}, nil
}

View File

@@ -6,10 +6,12 @@ import (
"ngrok/log"
"ngrok/proto"
"ngrok/util"
"unicode/utf8"
)
const (
size = 10
size = 10
pathMaxLength = 25
)
type HttpView struct {
@@ -69,7 +71,8 @@ func (v *HttpView) Render() {
v.Printf(0, 1, "-------------")
for i, obj := range v.HttpRequests.Slice() {
txn := obj.(*proto.HttpTxn)
v.Printf(0, 3+i, "%s %v", txn.Req.Method, txn.Req.URL.Path)
path := truncatePath(txn.Req.URL.Path)
v.Printf(0, 3+i, "%s %v", txn.Req.Method, path)
if txn.Resp != nil {
v.APrintf(colorFor(txn.Resp.Status), 30, 3+i, "%s", txn.Resp.Status)
}
@@ -80,3 +83,37 @@ func (v *HttpView) Render() {
func (v *HttpView) Shutdown() {
close(v.shutdown)
}
func truncatePath(path string) string {
// Truncate all long strings based on rune count
if utf8.RuneCountInString(path) > pathMaxLength {
path = string([]rune(path)[:pathMaxLength])
}
// By this point, len(path) should be < pathMaxLength if we're dealing with single-byte runes.
// Otherwise, we have a multi-byte string and need to calculate the size of each rune and
// truncate manually.
//
// This is a workaround for a bug in termbox-go. Remove it when this issue is fixed:
// https://github.com/nsf/termbox-go/pull/21
if len(path) > pathMaxLength {
out := make([]byte, pathMaxLength, pathMaxLength)
length := 0
for {
r, size := utf8.DecodeRuneInString(path[length:])
if r == utf8.RuneError && size == 1 {
break
}
// utf8.EncodeRune expects there to be enough room to store the full size of the rune
if length+size <= pathMaxLength {
utf8.EncodeRune(out[length:], r)
length += size
} else {
break
}
}
path = string(out[:length])
}
return path
}

View File

@@ -2,7 +2,6 @@
package term
import (
"fmt"
termbox "github.com/nsf/termbox-go"
"ngrok/client/mvc"
"ngrok/log"
@@ -108,11 +107,7 @@ func (v *TermView) draw() {
v.Printf(0, i, "%-30s%s -> %s", "Forwarding", t.PublicUrl, t.LocalAddr)
i++
}
webAddr := fmt.Sprintf("http://localhost:%d", v.ctl.GetWebViewPort())
if v.ctl.GetWebViewPort() == -1 {
webAddr = "disabled"
}
v.Printf(0, i+0, "%-30s%s", "Web Interface", webAddr)
v.Printf(0, i+0, "%-30s%s", "Web Interface", v.ctl.GetWebInspectAddr())
connMeter, connTimer := state.GetConnectionMetrics()
v.Printf(0, i+1, "%-30s%d", "# Conn", connMeter.Count())

View File

@@ -2,7 +2,6 @@
package web
import (
"fmt"
"github.com/garyburd/go-websocket/websocket"
"net/http"
"ngrok/client/assets"
@@ -22,7 +21,7 @@ type WebView struct {
wsMessages *util.Broadcast
}
func NewWebView(ctl mvc.Controller, port int) *WebView {
func NewWebView(ctl mvc.Controller, addr string) *WebView {
wv := &WebView{
Logger: log.NewPrefixLogger("view", "web"),
wsMessages: util.NewBroadcast(),
@@ -66,8 +65,8 @@ func NewWebView(ctl mvc.Controller, port int) *WebView {
w.Write(buf)
})
wv.Info("Serving web interface on localhost:%d", port)
wv.ctl.Go(func() { http.ListenAndServe(fmt.Sprintf(":%d", port), nil) })
wv.Info("Serving web interface on %s", addr)
wv.ctl.Go(func() { http.ListenAndServe(addr, nil) })
return wv
}

View File

@@ -4,12 +4,15 @@ import (
"bufio"
"bytes"
"crypto/tls"
"encoding/base64"
"fmt"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"ngrok/log"
"sync"
)
type Conn interface {
@@ -17,9 +20,11 @@ type Conn interface {
log.Logger
Id() string
SetType(string)
CloseRead() error
}
type loggedConn struct {
tcp *net.TCPConn
net.Conn
log.Logger
id int32
@@ -32,9 +37,16 @@ type Listener struct {
}
func wrapConn(conn net.Conn, typ string) *loggedConn {
c := &loggedConn{conn, log.NewPrefixLogger(), rand.Int31(), typ}
c.AddLogPrefix(c.Id())
return c
switch c := conn.(type) {
case *loggedConn:
return c
case *net.TCPConn:
wrapped := &loggedConn{c, conn, log.NewPrefixLogger(), rand.Int31(), typ}
wrapped.AddLogPrefix(wrapped.Id())
return wrapped
}
return nil
}
func Listen(addr, typ string, tlsCfg *tls.Config) (l *Listener, err error) {
@@ -88,17 +100,43 @@ func Dial(addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
return
}
func DialHttpProxy(proxyAddr, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
func DialHttpProxy(proxyUrl, addr, typ string, tlsCfg *tls.Config) (conn *loggedConn, err error) {
// parse the proxy address
var parsedUrl *url.URL
if parsedUrl, err = url.Parse(proxyUrl); err != nil {
return
}
var proxyAuth string
if parsedUrl.User != nil {
proxyAuth = "Basic " + base64.StdEncoding.EncodeToString([]byte(parsedUrl.User.String()))
}
var proxyTlsConfig *tls.Config
switch parsedUrl.Scheme {
case "http":
proxyTlsConfig = nil
case "https":
proxyTlsConfig = new(tls.Config)
default:
err = fmt.Errorf("Proxy URL scheme must be http or https, got: %s", parsedUrl.Scheme)
return
}
// dial the proxy
if conn, err = Dial(proxyAddr, typ, nil); err != nil {
if conn, err = Dial(parsedUrl.Host, typ, proxyTlsConfig); err != nil {
return
}
// send an HTTP proxy CONNECT message
req, err := http.NewRequest("CONNECT", "http://"+addr, nil)
req, err := http.NewRequest("CONNECT", "https://"+addr, nil)
if err != nil {
return
}
if proxyAuth != "" {
req.Header.Set("Proxy-Authorization", proxyAuth)
}
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; ngrok)")
req.Write(conn)
@@ -124,9 +162,11 @@ func (c *loggedConn) StartTLS(tlsCfg *tls.Config) {
c.Conn = tls.Client(c.Conn, tlsCfg)
}
func (c *loggedConn) Close() error {
c.Debug("Closing")
return c.Conn.Close()
func (c *loggedConn) Close() (err error) {
if err := c.Conn.Close(); err == nil {
c.Debug("Closing")
}
return
}
func (c *loggedConn) Id() string {
@@ -141,28 +181,37 @@ func (c *loggedConn) SetType(typ string) {
c.Info("Renamed connection %s", oldId)
}
func (c *loggedConn) CloseRead() error {
// XXX: use CloseRead() in Conn.Join() and in Control.shutdown() for cleaner
// connection termination. Unfortunately, when I've tried that, I've observed
// failures where the connection was closed *before* flushing its write buffer,
// set with SetLinger() set properly (which it is by default).
return c.tcp.CloseRead()
}
func Join(c Conn, c2 Conn) (int64, int64) {
done := make(chan error)
var wait sync.WaitGroup
pipe := func(to Conn, from Conn, bytesCopied *int64) {
defer to.Close()
defer from.Close()
defer wait.Done()
var err error
*bytesCopied, err = io.Copy(to, from)
if err != nil {
from.Warn("Copied %d bytes to %s before failing with error %v", *bytesCopied, to.Id(), err)
done <- err
} else {
from.Debug("Copied %d bytes from to %s", *bytesCopied, to.Id())
done <- nil
from.Debug("Copied %d bytes to %s", *bytesCopied, to.Id())
}
}
wait.Add(2)
var fromBytes, toBytes int64
go pipe(c, c2, &fromBytes)
go pipe(c2, c, &toBytes)
c.Info("Joined with connection %s", c2.Id())
<-done
c.Close()
c2.Close()
<-done
wait.Wait()
return fromBytes, toBytes
}

View File

@@ -63,11 +63,16 @@ type AuthResp struct {
// ReqId is a random number set by the client that it can pull
// from future NewTunnel's to correlate then to the requesting ReqTunnel.
type ReqTunnel struct {
ReqId string
Protocol string
ReqId string
Protocol string
// http only
Hostname string
Subdomain string
HttpAuth string
// tcp only
RemotePort uint16
}
// When the server opens a new tunnel on behalf of

View File

@@ -65,6 +65,8 @@ func (h *Http) WrapConn(c conn.Conn, ctx interface{}) conn.Conn {
}
func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interface{}) {
defer close(lastTxn)
for {
req, err := http.ReadRequest(tee.WriteBuffer())
if err != nil {
@@ -95,9 +97,7 @@ func (h *Http) readRequests(tee *conn.Tee, lastTxn chan *HttpTxn, connCtx interf
}
func (h *Http) readResponses(tee *conn.Tee, lastTxn chan *HttpTxn) {
for {
var err error
txn := <-lastTxn
for txn := range lastTxn {
resp, err := http.ReadResponse(tee.ReadBuffer(), txn.Req.Request)
txn.Duration = time.Since(txn.Start)
h.reqTimer.Update(txn.Duration)

View File

@@ -9,6 +9,8 @@ type Options struct {
httpsAddr string
tunnelAddr string
domain string
tlsCrt string
tlsKey string
logto string
}
@@ -17,6 +19,8 @@ func parseArgs() *Options {
httpsAddr := flag.String("httpsAddr", ":443", "Public address listening for HTTPS connections, emptry string to disable")
tunnelAddr := flag.String("tunnelAddr", ":4443", "Public address listening for ngrok client")
domain := flag.String("domain", "ngrok.com", "Domain where the tunnels are hosted")
tlsCrt := flag.String("tlsCrt", "", "Path to a TLS certificate file")
tlsKey := flag.String("tlsKey", "", "Path to a TLS key file")
logto := flag.String("log", "stdout", "Write log messages to this file. 'stdout' and 'none' have special meanings")
flag.Parse()
@@ -26,6 +30,8 @@ func parseArgs() *Options {
httpsAddr: *httpsAddr,
tunnelAddr: *tunnelAddr,
domain: *domain,
tlsCrt: *tlsCrt,
tlsKey: *tlsKey,
logto: *logto,
}
}

View File

@@ -9,13 +9,15 @@ import (
"ngrok/version"
"runtime/debug"
"strings"
"sync/atomic"
"time"
)
const (
pingTimeoutInterval = 30 * time.Second
connReapInterval = 10 * time.Second
controlWriteTimeout = 10 * time.Second
proxyStaleDuration = 60 * time.Second
proxyMaxPoolSize = 10
)
type Control struct {
@@ -33,11 +35,6 @@ type Control struct {
// to us over conn by the client
in chan (msg.Message)
// put a message in this channel to send it over
// conn to the client and then terminate this
// control connection and all of its tunnels
stop chan (msg.Message)
// the last time we received a ping from the client - for heartbeats
lastPing time.Time
@@ -47,27 +44,37 @@ type Control struct {
// proxy connections
proxies chan conn.Conn
// closing indicator
closing int32
// identifier
id string
// synchronizer for controlled shutdown of writer()
writerShutdown *util.Shutdown
// synchronizer for controlled shutdown of reader()
readerShutdown *util.Shutdown
// synchronizer for controlled shutdown of manager()
managerShutdown *util.Shutdown
// synchronizer for controller shutdown of entire Control
shutdown *util.Shutdown
}
func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
var err error
// create the object
// channels are buffered because we read and write to them
// from the same goroutine in managerThread()
c := &Control{
auth: authMsg,
conn: ctlConn,
out: make(chan msg.Message, 5),
in: make(chan msg.Message, 5),
stop: make(chan msg.Message, 5),
proxies: make(chan conn.Conn, 10),
lastPing: time.Now(),
auth: authMsg,
conn: ctlConn,
out: make(chan msg.Message),
in: make(chan msg.Message),
proxies: make(chan conn.Conn, 10),
lastPing: time.Now(),
writerShutdown: util.NewShutdown(),
readerShutdown: util.NewShutdown(),
managerShutdown: util.NewShutdown(),
shutdown: util.NewShutdown(),
}
failAuth := func(e error) {
@@ -85,14 +92,24 @@ func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
}
}
// set logging prefix
ctlConn.SetType("ctl")
ctlConn.AddLogPrefix(c.id)
if authMsg.Version != version.Proto {
failAuth(fmt.Errorf("Incompatible versions. Server %s, client %s. Download a new version at http://ngrok.com", version.MajorMinor(), authMsg.Version))
return
}
// register the control
controlRegistry.Add(c.id, c)
if replaced := controlRegistry.Add(c.id, c); replaced != nil {
replaced.shutdown.WaitComplete()
}
// start the writer first so that the following messages get sent
go c.writer()
// Respond to authentication
c.out <- &msg.AuthResp{
Version: version.Proto,
MmVersion: version.MajorMinor(),
@@ -102,12 +119,10 @@ func NewControl(ctlConn conn.Conn, authMsg *msg.Auth) {
// As a performance optimization, ask for a proxy connection up front
c.out <- &msg.ReqProxy{}
// set logging prefix
ctlConn.SetType("ctl")
// manage the connection
go c.managerThread()
go c.readThread()
go c.manager()
go c.reader()
go c.stopper()
}
// Register a new tunnel on this control connection
@@ -119,14 +134,9 @@ func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
c.conn.Debug("Registering new tunnel")
t, err := NewTunnel(&tunnelReq, c)
if err != nil {
ack := &msg.NewTunnel{Error: err.Error()}
c.out <- &msg.NewTunnel{Error: err.Error()}
if len(c.tunnels) == 0 {
// you can't fail your first tunnel registration
// terminate the control connection
c.stop <- ack
} else {
// inform client of failure
c.out <- ack
c.shutdown.Begin()
}
// we're done
@@ -140,67 +150,45 @@ func (c *Control) registerTunnel(rawTunnelReq *msg.ReqTunnel) {
c.out <- &msg.NewTunnel{
Url: t.url,
Protocol: proto,
ReqId: rawTunnelReq.ReqId,
}
rawTunnelReq.Hostname = strings.Replace(t.url, proto+"://", "", 1)
}
}
func (c *Control) managerThread() {
reap := time.NewTicker(connReapInterval)
// all shutdown functionality in here
func (c *Control) manager() {
// don't crash on panics
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::managerThread failed with error %v: %s", err, debug.Stack())
c.conn.Info("Control::manager failed with error %v: %s", err, debug.Stack())
}
// remove from the control registry
controlRegistry.Del(c.id)
// mark that we're shutting down
atomic.StoreInt32(&c.closing, 1)
// stop the reaping timer
reap.Stop()
// close the connection
c.conn.Close()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// we're safe to close(c.proxies) because c.closing
// protects us inside of RegisterProxy
close(c.proxies)
// shut down all of the proxy connections
for p := range c.proxies {
p.Close()
}
}()
// kill everything if the control manager stops
defer c.shutdown.Begin()
// notify that manager() has shutdown
defer c.managerShutdown.Complete()
// reaping timer for detecting heartbeat failure
reap := time.NewTicker(connReapInterval)
defer reap.Stop()
for {
select {
case m := <-c.out:
msg.WriteMsg(c.conn, m)
case m := <-c.stop:
if m != nil {
msg.WriteMsg(c.conn, m)
}
return
case <-reap.C:
if time.Since(c.lastPing) > pingTimeoutInterval {
c.conn.Info("Lost heartbeat")
c.shutdown.Begin()
}
case mRaw, ok := <-c.in:
// c.in closes to indicate shutdown
if !ok {
return
}
case mRaw := <-c.in:
switch m := mRaw.(type) {
case *msg.ReqTunnel:
c.registerTunnel(m)
@@ -213,14 +201,41 @@ func (c *Control) managerThread() {
}
}
func (c *Control) readThread() {
func (c *Control) writer() {
defer func() {
if err := recover(); err != nil {
c.conn.Info("Control::readThread failed with error %v: %s", err, debug.Stack())
c.conn.Info("Control::writer failed with error %v: %s", err, debug.Stack())
}
c.stop <- nil
}()
// kill everything if the writer() stops
defer c.shutdown.Begin()
// notify that we've flushed all messages
defer c.writerShutdown.Complete()
// write messages to the control channel
for m := range c.out {
c.conn.SetWriteDeadline(time.Now().Add(controlWriteTimeout))
if err := msg.WriteMsg(c.conn, m); err != nil {
panic(err)
}
}
}
func (c *Control) reader() {
defer func() {
if err := recover(); err != nil {
c.conn.Warn("Control::reader failed with error %v: %s", err, debug.Stack())
}
}()
// kill everything if the reader stops
defer c.shutdown.Begin()
// notify that we're done
defer c.readerShutdown.Complete()
// read messages from the control channel
for {
if msg, err := msg.ReadMsg(c.conn); err != nil {
@@ -231,23 +246,60 @@ func (c *Control) readThread() {
panic(err)
}
} else {
// this can also panic during shutdown
c.in <- msg
}
}
}
func (c *Control) RegisterProxy(conn conn.Conn) {
if atomic.LoadInt32(&c.closing) == 1 {
c.conn.Debug("Can't register proxies for a control that is closing")
conn.Close()
return
func (c *Control) stopper() {
defer func() {
if r := recover(); r != nil {
c.conn.Error("Failed to shut down control: %v", r)
}
}()
// wait until we're instructed to shutdown
c.shutdown.WaitBegin()
// remove ourself from the control registry
controlRegistry.Del(c.id)
// shutdown manager() so that we have no more work to do
close(c.in)
c.managerShutdown.WaitComplete()
// shutdown writer()
close(c.out)
c.writerShutdown.WaitComplete()
// close connection fully
c.conn.Close()
// shutdown all of the tunnels
for _, t := range c.tunnels {
t.Shutdown()
}
// shutdown all of the proxy connections
close(c.proxies)
for p := range c.proxies {
p.Close()
}
c.shutdown.Complete()
c.conn.Info("Shutdown complete")
}
func (c *Control) RegisterProxy(conn conn.Conn) {
conn.AddLogPrefix(c.id)
conn.SetDeadline(time.Now().Add(proxyStaleDuration))
select {
case c.proxies <- conn:
c.conn.Info("Registered proxy connection %s", conn.Id())
conn.Info("Registered")
default:
// c.proxies buffer is full, discard this one
conn.Info("Proxies buffer is full, discarding.")
conn.Close()
}
}
@@ -258,37 +310,47 @@ func (c *Control) RegisterProxy(conn conn.Conn) {
// Returns an error if we couldn't get a proxy because it took too long
// or the tunnel is closing
func (c *Control) GetProxy() (proxyConn conn.Conn, err error) {
// initial timeout is zero to try to get a proxy connection without asking for one
timeout := time.NewTimer(0)
var ok bool
// get a proxy connection from the pool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
default:
// no proxy available in the pool, ask for one over the control channel
c.conn.Debug("No proxy in pool, requesting proxy from control . . .")
if err = util.PanicToError(func() { c.out <- &msg.ReqProxy{} }); err != nil {
return
}
// get a proxy connection. if we timeout, request one over the control channel
for proxyConn == nil {
var ok bool
select {
case proxyConn, ok = <-c.proxies:
if !ok {
err = fmt.Errorf("No proxy connections available, control is closing")
return
}
continue
case <-timeout.C:
c.conn.Debug("Requesting new proxy connection")
// request a proxy connection
c.out <- &msg.ReqProxy{}
// timeout after 1 second if we don't get one
timeout.Reset(1 * time.Second)
case <-time.After(pingTimeoutInterval):
err = fmt.Errorf("Timeout trying to get proxy connection")
return
}
}
// To try to reduce latency hanndling tunnel connections, we employ
// the following curde heuristic:
// If the proxy connection pool is empty, request a new one.
// The idea is to always have at least one proxy connection available for immediate use.
// There are two major issues with this strategy: it's not thread safe and it's not predictive.
// It should be a good start though.
if len(c.proxies) == 0 {
c.out <- &msg.ReqProxy{}
}
return
}
// Called when this control is replaced by another control
// this can happen if the network drops out and the client reconnects
// before the old tunnel has lost its heartbeat
func (c *Control) Replaced(replacement *Control) {
c.conn.Info("Replaced by control: %s", replacement.conn.Id())
// set the control id to empty string so that when stopper()
// calls registry.Del it won't delete the replacement
c.id = ""
// tell the old one to shutdown
c.shutdown.Begin()
}

View File

@@ -7,6 +7,7 @@ import (
"ngrok/conn"
"ngrok/log"
"strings"
"time"
)
const (
@@ -66,6 +67,9 @@ func httpHandler(tcpConn net.Conn, proto string) {
}
}()
// Make sure we detect dead connections while we decide how to multiplex
conn.SetDeadline(time.Now().Add(connReadTimeout))
// read out the http request
req, err := conn.ReadRequest()
if err != nil {
@@ -95,5 +99,9 @@ func httpHandler(tcpConn net.Conn, proto string) {
return
}
// dead connections will now be handled by tunnel heartbeating and the client
conn.SetDeadline(time.Time{})
// let the tunnel handle the connection now
tunnel.HandlePublicConnection(conn)
}

View File

@@ -1,16 +1,20 @@
package server
import (
"crypto/tls"
"math/rand"
"ngrok/conn"
log "ngrok/log"
"ngrok/msg"
"ngrok/util"
"os"
"runtime/debug"
"time"
)
const (
registryCacheSize uint64 = 1024 * 1024 // 1 MB
registryCacheSize uint64 = 1024 * 1024 // 1 MB
connReadTimeout time.Duration = 10 * time.Second
)
// GLOBALS
@@ -51,7 +55,7 @@ func NewProxy(pxyConn conn.Conn, regPxy *msg.RegProxy) {
// for ease of deployment. The hope is that by running on port 443, using
// TLS and running all connections over the same port, we can bust through
// restrictive firewalls.
func tunnelListener(addr string) {
func tunnelListener(addr string, tlsConfig *tls.Config) {
// listen for incoming connections
listener, err := conn.Listen(addr, "tun", tlsConfig)
if err != nil {
@@ -60,19 +64,37 @@ func tunnelListener(addr string) {
log.Info("Listening for control and proxy connections on %s", listener.Addr.String())
for c := range listener.Conns {
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(c); err != nil {
c.Error("Failed to read message: %v", err)
c.Close()
}
go func(tunnelConn conn.Conn) {
// don't crash on panics
defer func() {
if r := recover(); r != nil {
tunnelConn.Info("tunnelListener failed with error %v: %s", r, debug.Stack())
}
}()
switch m := rawMsg.(type) {
case *msg.Auth:
go NewControl(c, m)
tunnelConn.SetReadDeadline(time.Now().Add(connReadTimeout))
var rawMsg msg.Message
if rawMsg, err = msg.ReadMsg(tunnelConn); err != nil {
tunnelConn.Warn("Failed to read message: %v", err)
tunnelConn.Close()
return
}
case *msg.RegProxy:
go NewProxy(c, m)
}
// don't timeout after the initial read, tunnel heartbeating will kill
// dead connections
tunnelConn.SetReadDeadline(time.Time{})
switch m := rawMsg.(type) {
case *msg.Auth:
NewControl(tunnelConn, m)
case *msg.RegProxy:
NewProxy(tunnelConn, m)
default:
tunnelConn.Close()
}
}(c)
}
}
@@ -98,6 +120,12 @@ func Main() {
// start listeners
listeners = make(map[string]*conn.Listener)
// load tls configuration
tlsConfig, err := LoadTLSConfig(opts.tlsCrt, opts.tlsKey)
if err != nil {
panic(err)
}
// listen for http
if opts.httpAddr != "" {
listeners["http"] = startHttpListener(opts.httpAddr, nil)
@@ -109,5 +137,5 @@ func Main() {
}
// ngrok clients
tunnelListener(opts.tunnelAddr)
tunnelListener(opts.tunnelAddr, tlsConfig)
}

View File

@@ -19,7 +19,7 @@ func init() {
keenApiKey := os.Getenv("KEEN_API_KEY")
if keenApiKey != "" {
metrics = NewKeenIoMetrics()
metrics = NewKeenIoMetrics(60 * time.Second)
} else {
metrics = NewLocalMetrics(30 * time.Second)
}
@@ -154,9 +154,9 @@ func (m *LocalMetrics) Report() {
}
}
type KeenIoRequest struct {
Path string
Body []byte
type KeenIoMetric struct {
Collection string
Event interface{}
}
type KeenIoMetrics struct {
@@ -164,20 +164,54 @@ type KeenIoMetrics struct {
ApiKey string
ProjectToken string
HttpClient http.Client
Requests chan *KeenIoRequest
Metrics chan *KeenIoMetric
}
func NewKeenIoMetrics() *KeenIoMetrics {
func NewKeenIoMetrics(batchInterval time.Duration) *KeenIoMetrics {
k := &KeenIoMetrics{
Logger: log.NewPrefixLogger("metrics"),
ApiKey: os.Getenv("KEEN_API_KEY"),
ProjectToken: os.Getenv("KEEN_PROJECT_TOKEN"),
Requests: make(chan *KeenIoRequest, 100),
Metrics: make(chan *KeenIoMetric, 1000),
}
go func() {
for req := range k.Requests {
k.AuthedRequest("POST", req.Path, bytes.NewReader(req.Body))
defer func() {
if r := recover(); r != nil {
k.Error("KeenIoMetrics failed: %v", r)
}
}()
batch := make(map[string][]interface{})
batchTimer := time.Tick(batchInterval)
for {
select {
case m := <-k.Metrics:
list, ok := batch[m.Collection]
if !ok {
list = make([]interface{}, 0)
}
batch[m.Collection] = append(list, m.Event)
case <-batchTimer:
// no metrics to report
if len(batch) == 0 {
continue
}
payload, err := json.Marshal(batch)
if err != nil {
k.Error("Failed to serialize metrics payload: %v, %v", batch, err)
} else {
for key, val := range batch {
k.Debug("Reporting %d metrics for %s", len(val), key)
}
k.AuthedRequest("POST", "/events", bytes.NewReader(payload))
}
batch = make(map[string][]interface{})
}
}
}()
@@ -198,13 +232,15 @@ func (k *KeenIoMetrics) AuthedRequest(method, path string, body *bytes.Reader) (
req.ContentLength = int64(body.Len())
}
requestStartAt := time.Now()
resp, err = k.HttpClient.Do(req)
if err != nil {
k.Error("Failed to send metric event to keen.io %v", err)
} else {
k.Info("keen.io processed request in %f sec", time.Since(requestStartAt).Seconds())
defer resp.Body.Close()
if resp.StatusCode != 201 {
if resp.StatusCode != 200 {
bytes, _ := ioutil.ReadAll(resp.Body)
k.Error("Got %v response from keen.io: %s", resp.StatusCode, bytes)
}
@@ -217,7 +253,7 @@ func (k *KeenIoMetrics) OpenConnection(t *Tunnel, c conn.Conn) {
}
func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time, in, out int64) {
buf, err := json.Marshal(struct {
event := struct {
Keen KeenStruct `json:"keen"`
OS string
ClientId string
@@ -248,13 +284,9 @@ func (k *KeenIoMetrics) CloseConnection(t *Tunnel, c conn.Conn, start time.Time,
ConnectionDuration: time.Since(start).Seconds(),
BytesIn: in,
BytesOut: out,
})
if err != nil {
k.Error("Error serializing metric %v", err)
} else {
k.Requests <- &KeenIoRequest{Path: "/events/CloseConnection", Body: buf}
}
k.Metrics <- &KeenIoMetric{Collection: "CloseConnection", Event: event}
}
func (k *KeenIoMetrics) OpenTunnel(t *Tunnel) {
@@ -265,7 +297,7 @@ type KeenStruct struct {
}
func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
buf, err := json.Marshal(struct {
event := struct {
Keen KeenStruct `json:"keen"`
OS string
ClientId string
@@ -291,12 +323,7 @@ func (k *KeenIoMetrics) CloseTunnel(t *Tunnel) {
Duration: time.Since(t.start).Seconds(),
HttpAuth: t.req.HttpAuth != "",
Subdomain: t.req.Subdomain != "",
})
if err != nil {
k.Error("Error serializing metric %v", err)
return
} else {
k.Requests <- &KeenIoRequest{Path: "/events/CloseTunnel", Body: buf}
}
k.Metrics <- &KeenIoMetric{Collection: "CloseTunnel", Event: event}
}

View File

@@ -180,11 +180,18 @@ func (r *ControlRegistry) Get(clientId string) *Control {
return r.controls[clientId]
}
func (r *ControlRegistry) Add(clientId string, ctl *Control) {
func (r *ControlRegistry) Add(clientId string, ctl *Control) (oldCtl *Control) {
r.Lock()
defer r.Unlock()
oldCtl = r.controls[clientId]
if oldCtl != nil {
oldCtl.Replaced(ctl)
}
r.controls[clientId] = ctl
r.Info("Registered control with id %s", clientId)
return
}
func (r *ControlRegistry) Del(clientId string) error {

View File

@@ -4,40 +4,40 @@ import (
"crypto/tls"
"io/ioutil"
"ngrok/server/assets"
"os"
)
var (
tlsConfig *tls.Config
)
func init() {
readOrBytes := func(envVar string, default_path string) []byte {
f := os.Getenv(envVar)
if f == "" {
b, err := assets.ReadAsset(default_path)
if err != nil {
panic(err)
}
return b
} else {
if b, err := ioutil.ReadFile(f); err != nil {
panic(err)
} else {
return b
}
func LoadTLSConfig(crtPath string, keyPath string) (tlsConfig *tls.Config, err error) {
fileOrAsset := func(path string, default_path string) ([]byte, error) {
loadFn := ioutil.ReadFile
if path == "" {
loadFn = assets.ReadAsset
path = default_path
}
return loadFn(path)
}
crt := readOrBytes("TLS_CRT_FILE", "assets/server/tls/snakeoil.crt")
key := readOrBytes("TLS_KEY_FILE", "assets/server/tls/snakeoil.key")
cert, err := tls.X509KeyPair(crt, key)
var (
crt []byte
key []byte
cert tls.Certificate
)
if err != nil {
panic(err)
if crt, err = fileOrAsset(crtPath, "assets/server/tls/snakeoil.crt"); err != nil {
return
}
if key, err = fileOrAsset(keyPath, "assets/server/tls/snakeoil.key"); err != nil {
return
}
if cert, err = tls.X509KeyPair(crt, key); err != nil {
return
}
tlsConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
}
return
}

View File

@@ -8,6 +8,7 @@ import (
"ngrok/conn"
"ngrok/log"
"ngrok/msg"
"ngrok/util"
"os"
"strconv"
"strings"
@@ -104,50 +105,58 @@ func NewTunnel(m *msg.ReqTunnel, ctl *Control) (t *Tunnel, err error) {
proto := t.req.Protocol
switch proto {
case "tcp":
var port int = 0
bindTcp := func(port int) error {
if t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port}); err != nil {
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
return err
}
// create the url
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
// register it
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
return err
}
go t.listenTcp(t.listener)
return nil
}
// use the custom remote port you asked for
if t.req.RemotePort != 0 {
bindTcp(int(t.req.RemotePort))
return
}
// try to return to you the same port you had before
cachedUrl := tunnelRegistry.GetCachedRegistration(t)
if cachedUrl != "" {
var port int
parts := strings.Split(cachedUrl, ":")
portPart := parts[len(parts)-1]
port, err = strconv.Atoi(portPart)
if err != nil {
t.ctl.conn.Error("Failed to parse cached url port as integer: %s", portPart)
// continue with zero
port = 0
} else {
// we have a valid, cached port, let's try to bind with it
if bindTcp(port) != nil {
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
} else {
// success, we're done
return
}
}
}
// Bind for TCP connections
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: port})
// If we failed with a custom port, try with a random one
if err != nil && port != 0 {
t.ctl.conn.Warn("Failed to get custom port %d: %v, trying a random one", port, err)
t.listener, err = net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("0.0.0.0"), Port: 0})
}
// we tried to bind with a random port and failed (no more ports available?)
if err != nil {
err = t.ctl.conn.Error("Error binding TCP listener: %v", err)
return
}
// create the url
addr := t.listener.Addr().(*net.TCPAddr)
t.url = fmt.Sprintf("tcp://%s:%d", opts.domain, addr.Port)
// register it
if err = tunnelRegistry.RegisterAndCache(t.url, t); err != nil {
// This should never be possible because the OS will
// only assign available ports to us.
t.listener.Close()
err = fmt.Errorf("TCP listener bound, but failed to register %s", t.url)
return
}
go t.listenTcp(t.listener)
bindTcp(0)
return
case "http", "https":
l, ok := listeners[proto]
@@ -245,9 +254,8 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
metrics.OpenConnection(t, publicConn)
var proxyConn conn.Conn
var attempts int
var err error
for {
for i := 0; i < (2 * proxyMaxPoolSize); i++ {
// get a proxy connection
if proxyConn, err = t.ctl.GetProxy(); err != nil {
t.Warn("Failed to get proxy connection: %v", err)
@@ -262,20 +270,29 @@ func (t *Tunnel) HandlePublicConnection(publicConn conn.Conn) {
Url: t.url,
ClientAddr: publicConn.RemoteAddr().String(),
}
if err = msg.WriteMsg(proxyConn, startPxyMsg); err != nil {
attempts += 1
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, attempts)
if attempts > 3 {
// give up
publicConn.Error("Too many failures starting proxy connection")
return
}
proxyConn.Warn("Failed to write StartProxyMessage: %v, attempt %d", err, i)
proxyConn.Close()
} else {
// success
break
}
}
if err != nil {
// give up
publicConn.Error("Too many failures starting proxy connection")
return
}
// To reduce latency handling tunnel connections, we employ the following curde heuristic:
// Whenever we take a proxy connection from the pool, replace it with a new one
util.PanicToError(func() { t.ctl.out <- &msg.ReqProxy{} })
// no timeouts while connections are joined
proxyConn.SetDeadline(time.Time{})
// join the public and proxy connections
bytesIn, bytesOut := conn.Join(publicConn, proxyConn)
metrics.CloseConnection(t, publicConn, startTime, bytesIn, bytesOut)

33
src/ngrok/util/errors.go Normal file
View File

@@ -0,0 +1,33 @@
package util
import (
"fmt"
"runtime"
)
const crashMessage = `panic: %v
%s
Oh noes! ngrok crashed!
Please submit the stack trace and any relevant information to:
github.com/inconshreveable/ngrok/issues`
func MakePanicTrace(err interface{}) string {
stackBuf := make([]byte, 4096)
n := runtime.Stack(stackBuf, false)
return fmt.Sprintf(crashMessage, err, stackBuf[:n])
}
// Runs the given function and converts any panic encountered while doing so
// into an error. Useful for sending to channels that will close
func PanicToError(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("Panic: %v", r)
}
}()
fn()
return
}

View File

@@ -0,0 +1,43 @@
package util
import (
"sync"
)
// A small utility class for managing controlled shutdowns
type Shutdown struct {
sync.Mutex
inProgress bool
begin chan int // closed when the shutdown begins
complete chan int // closed when the shutdown completes
}
func NewShutdown() *Shutdown {
return &Shutdown{
begin: make(chan int),
complete: make(chan int),
}
}
func (s *Shutdown) Begin() {
s.Lock()
defer s.Unlock()
if s.inProgress == true {
return
} else {
s.inProgress = true
close(s.begin)
}
}
func (s *Shutdown) WaitBegin() {
<-s.begin
}
func (s *Shutdown) Complete() {
close(s.complete)
}
func (s *Shutdown) WaitComplete() {
<-s.complete
}

View File

@@ -1,12 +0,0 @@
package util
import (
"fmt"
"runtime"
)
func MakePanicTrace(err interface{}) string {
stackBuf := make([]byte, 4096)
n := runtime.Stack(stackBuf, false)
return fmt.Sprintf("panic: %v\n\n%s", err, stackBuf[:n])
}

View File

@@ -7,7 +7,7 @@ import (
const (
Proto = "2"
Major = "1"
Minor = "1"
Minor = "6"
)
func MajorMinor() string {