Skip to content

Commit

Permalink
cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
mmatczuk committed Oct 12, 2016
1 parent 6f33ae2 commit 5707fa1
Show file tree
Hide file tree
Showing 16 changed files with 170 additions and 81 deletions.
18 changes: 16 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,16 @@
.idea
*.iml
### IntelliJ
.idea/
*.iml

### Vim
# swap
[._]*.s[a-w][a-z]
[._]s[a-w][a-z]
# session
Session.vim
# temporary
.netrwhist
*~
# auto-generated tag files
tags

32 changes: 32 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
language: go
sudo: false

go:
- 1.6
- tip

matrix:
fast_finish: true

addons:
apt:
packages:
- moreutils

before_install:
- go get -u golang.org/x/tools/cmd/cover
- go get -u github.com/golang/lint/golint
- go get -u github.com/mattn/goveralls

install:
- go get -d ./...
- go build ./...

script:
- gofmt -s -l . | ifne false
- go vet ./...
- $HOME/gopath/bin/golint ./...
- go test -covermode=count -coverprofile=profile.cov -race . ./proto

after_script:
- $HOME/gopath/bin/goveralls -coverprofile=profile.cov -service=travis-ci
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ With h2tun you can proxy:

## Benchmark

h2tun is benchmarked against [koding tunnel](https://github.com/koding/tunnel). h2tun proves to be more more stable, it can handle greater throughput with better latencies. [See benchmark report](benchmark/report/README.md) for more details.
h2tun is benchmarked against [koding tunnel](https://github.com/koding/tunnel). h2tun proves to be more stable, it can handle greater throughput with better latencies. [See benchmark report](benchmark/report/README.md) for more details.
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
1. Default proxy functions
1. Dynamic `AllowedClient` management
1. Client driven configuration, on connect client sends it's configuration, server just needs to know the certificate id
1. URL prefix based routing, like urlprefix tag in fabio https://github.com/eBay/fabio/wiki/Quickstart
1. Ping and RTT, like https://godoc.org/github.com/hashicorp/yamux#Session.Ping
1. Stream compression
1. `ControlMessage` `String()` function for better logging
Expand Down
4 changes: 2 additions & 2 deletions benchmark/cmd/h2tunclient/h2tunclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ func main() {
TLSClientConfig: h2tuntest.TLSConfig(cert),
Proxy: p,
})
if err := c.Connect(); err != nil {
if err := c.Start(); err != nil {
logging.Fatal("Client start failed: %s", err)
}
defer c.Close()
defer c.Stop()

select {}
}
18 changes: 7 additions & 11 deletions benchmark/report/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@

The benchmark compares h2tun to [koding tunnel](https://github.com/koding/tunnel) on serving 184 midsized files that were gathered by saving `amazon.com` for offline view. The data set consists of images and text data (js, css, html). On start client loads the files into memory and act as a file server.

The diagrams were rendered using [hdrhistogram](http://hdrhistogram.github.io/HdrHistogram/plotFiles.html) and the input files were generated with help of [github.com/codahale/hdrhistogram](https://github.com/codahale/hdrhistogram) library. The vegeta raw results were corrected for stalls using [hdr correction method](https://godoc.org/github.com/codahale/hdrhistogram#Histogram.RecordCorrectedValue).

## Environment

Tests were done on four AWS `t2.micro` instances. An instance for client, an instance for server and two instances for load generator. For load generation we used [vegeta](https://github.com/tsenart/vegeta) in distributed mode. Process open files limit was increased to `20000`.
Tests were done on four AWS `t2.micro` instances. An instance for client, an instance for server and two instances for load generator. For load generation we used [vegeta](https://github.com/tsenart/vegeta) in distributed mode. On all machines open files limit (`ulimit -n`) was increased to `20000`.

## Load spike

This test compares performance on two minute load spikes.

h2tun handles 900 req/sec without dropping a message and preserving a good latency. At 1000 req/sec h2tun still works but drops 0,20% requests and latency is much worse.

Koding tunnel is faster at 800 req/sec, but then latency degrades giving maximum values of 1.65s at 900 req/sec and 23.50s at 1000 req/sec (with 5% error rate).
This test compares performance on two minute load spikes. h2tun handles 900 req/sec without dropping a message while preserving good latency. At 1000 req/sec h2tun still works but drops 0,20% requests and latency is much worse. Koding tunnel is faster at 800 req/sec, but at higher request rates latency degrades giving maximum values of 1.65s at 900 req/sec and 23.50s at 1000 req/sec (with 5% error rate).

![](spike.png)

Expand All @@ -31,13 +29,11 @@ Detailed results of load spike test.

## Constant pressure

This test compares performance on twenty minutes constant pressure runs.

h2tun shows ability to trade latency for throughput. It runs just fine at 300 req/sec but at higher pace the bad things tend to aggregate resulting in increased latency or even message drops.
This test compares performance on twenty minutes constant pressure runs. h2tun shows ability to trade latency for throughput. It runs fine at 300 req/sec but at higher request rates we observe poor latency and some message drops. Koding tunnel has acceptable performance at 300 req/sec, however, with increased load it just breaks.

Koding tunnel also runs well at 300 req/sec, however, with increased load it just breaks.
Both implementations have a connection (or memory) leak when dealing with too high loads. This results in process (or machine) crash as machine runs out of memory. It's 100% reproducible, when process crashes it has few hundred thousands go routines waiting on select in a connection and memory full of connection buffers.

Both implementations have a connection (or memory) leak when dealing with too high loads. This results in process (or machine) crash as machine runs out of memory. It's 100% reproducible, when process crashes it has few hundred thousands go routines waiting on select in a connection and memory full of connection buffers. During the constant pressure tests it also happened that koding tunnel client crashed a (panic in yamux), that was never observed for h2tun.
During the constant pressure tests it also happened that koding tunnel client crashed a (panic in yamux), that was never observed for h2tun.

![](constload.png)

Expand Down
36 changes: 26 additions & 10 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,42 @@ import (
"fmt"
"net"
"net/http"
"sync"

"github.com/koding/h2tun/proto"
"github.com/koding/logging"
"golang.org/x/net/http2"
)

// ClientConfig is Client configuration object.
// ClientConfig defines configuration for the Client.
type ClientConfig struct {
// ServerAddr specifies TCP address of the tunnel server.
ServerAddr string
// TLSClientConfig specifies the TLS configuration to use with tls.Client.
// TLSClientConfig specifies the tls configuration to use with tls.Client.
TLSClientConfig *tls.Config
// DialTLS specifies an optional dial function for creating
// TLS connections for requests.
//
// If DialTLS is nil, tls.Dial is used.
// DialTLS specifies an optional dial function that creates a tls
// connection to the server. If DialTLS is nil, tls.Dial is used.
DialTLS func(network, addr string, config *tls.Config) (net.Conn, error)
// Proxy specifies proxying rules.
// Proxy is ProxyFunc responsible for transferring data between server
// and local services.
Proxy ProxyFunc
// Log specifies the logger. If nil a default logging.Logger is used.
Log logging.Logger
}

// Client is client to tunnel server.
// Client is responsible for creating connection to the server, handling control
// messages. It uses ProxyFunc for transferring data between server and local
// services.
type Client struct {
config *ClientConfig
conn net.Conn
connMu sync.Mutex
httpServer *http2.Server
log logging.Logger
}

// NewClient creates a new unconnected Client based on configuration. Caller
// must invoke Start() on returned instance in order to connect server.
func NewClient(config *ClientConfig) *Client {
log := logging.NewLogger("client")
if config.Log != nil {
Expand All @@ -51,7 +56,12 @@ func NewClient(config *ClientConfig) *Client {
return c
}

func (c *Client) Connect() error {
// Start connects client to the server, it returns error if there is a dial error,
// otherwise it spawns a new goroutine with http/2 server handling ControlMessages.
func (c *Client) Start() error {
c.connMu.Lock()
defer c.connMu.Unlock()

c.log.Info("Connecting to %q", c.config.ServerAddr)
conn, err := c.dial("tcp", c.config.ServerAddr, c.config.TLSClientConfig)
if err != nil {
Expand Down Expand Up @@ -90,9 +100,15 @@ func (c *Client) serveHTTP(w http.ResponseWriter, r *http.Request) {
c.log.Debug("Done proxying %v", msg)
}

func (c *Client) Close() error {
// Stop closes the connection between client and server. After stopping client
// can be started again.
func (c *Client) Stop() error {
c.connMu.Lock()
defer c.connMu.Unlock()

if c.conn == nil {
return nil
}
c.httpServer = nil
return c.conn.Close()
}
4 changes: 4 additions & 0 deletions h2tun.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Package h2tun is fast and secure server/client package that enables proxying
// public connections to your local machine over a tunnel connection from the
// local machine to the public server.
package h2tun
18 changes: 9 additions & 9 deletions h2tun_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const (

var payload = randPayload(payloadInitialSize, payloadLen)

func TestProxying(t *testing.T) {
func TestProxy(t *testing.T) {
t.Parallel()

cert, id := selfSignedCert()
Expand Down Expand Up @@ -56,23 +56,23 @@ func TestProxying(t *testing.T) {
TLSClientConfig: h2tuntest.TLSConfig(cert),
Proxy: h2tuntest.EchoProxyFunc,
})
if err := c.Connect(); err != nil {
if err := c.Start(); err != nil {
t.Error("Client start failed", err)
}
defer c.Close()
defer c.Stop()

data := []struct {
protocol string
repeat int
seq []uint
}{
{"http", 16, []uint{1000, 800, 600, 400, 200, 100}},
{"http", 8, []uint{200, 400, 600, 800, 1000}},
{"http", 4, []uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 1000}},
{"http", 4, []uint{100, 80, 60, 40, 20, 10}},
{"http", 2, []uint{20, 40, 60, 80, 100}},
{"http", 1, []uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 100}},

{"tcp", 16, []uint{1000, 800, 600, 400, 200, 100}},
{"tcp", 8, []uint{200, 400, 600, 800, 1000}},
{"tcp", 4, []uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 1000}},
{"tcp", 4, []uint{100, 80, 60, 40, 20, 10}},
{"tcp", 2, []uint{20, 40, 60, 80, 100}},
{"tcp", 1, []uint{0, 0, 0, 0, 0, 0, 0, 0, 0, 100}},
}

var wg sync.WaitGroup
Expand Down
14 changes: 10 additions & 4 deletions h2tuntest/h2tuntest.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/koding/logging"
)

// EchoProxyFunc pipes reader with writer.
func EchoProxyFunc(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
switch msg.Protocol {
case proto.HTTPProtocol:
Expand All @@ -29,6 +30,8 @@ func EchoProxyFunc(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
}
}

// EchoHTTPProxyFunc is a special case of EchoProxyFunc that handles HTTP
// request response model.
func EchoHTTPProxyFunc(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
req, err := http.ReadRequest(bufio.NewReader(r))
if err != nil {
Expand Down Expand Up @@ -57,6 +60,9 @@ func EchoHTTPProxyFunc(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage)
resp.Write(w)
}

// InMemoryFileServer scans directory dir, loads all files to memory and returns
// a http ProxyFunc that maps URL paths to relative filesystem paths i.e. file
// ./data/foo/bar.zip will be available under URL host:port/data/foo/bar.zip.
func InMemoryFileServer(dir string) (h2tun.ProxyFunc, error) {
dir, err := filepath.Abs(dir)
if err != nil {
Expand Down Expand Up @@ -87,10 +93,6 @@ func InMemoryFileServer(dir string) (h2tun.ProxyFunc, error) {
return nil, fmt.Errorf("failed to read directory %q: %s", dir, err)
}

for k, v := range mux {
logging.Info("Mux %q %dB", k, len(v))
}

return func(w io.Writer, r io.ReadCloser, msg *proto.ControlMessage) {
b, ok := mux[msg.URLPath]
if !ok {
Expand All @@ -109,6 +111,7 @@ func InMemoryFileServer(dir string) (h2tun.ProxyFunc, error) {
}, nil
}

// ResponseBytes returns http response containing file as body.
func ResponseBytes(file string) ([]byte, error) {
resp := &http.Response{
Status: "200 OK",
Expand Down Expand Up @@ -140,6 +143,8 @@ func ResponseBytes(file string) ([]byte, error) {
return b.Bytes(), nil
}

// TLSConfig returns valid http/2 tls configuration that can be used by both
// client and server.
func TLSConfig(cert tls.Certificate) *tls.Config {
c := &tls.Config{
Certificates: []tls.Certificate{cert},
Expand All @@ -155,6 +160,7 @@ func TLSConfig(cert tls.Certificate) *tls.Config {
return c
}

// DebugLogging makes koding logger print debug messages.
func DebugLogging() {
logging.DefaultLevel = logging.DEBUG
logging.DefaultHandler.SetLevel(logging.DEBUG)
Expand Down
8 changes: 4 additions & 4 deletions pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import (
)

var (
noClientConnErr = errors.New("no connection")
clientAlreadyConnectedErr = errors.New("client already connected")
errNoClientConn = errors.New("no connection")
errClientAlreadyConnected = errors.New("client already connected")
)

type connPool struct {
Expand All @@ -36,7 +36,7 @@ func (p *connPool) GetClientConn(req *http.Request, addr string) (*http2.ClientC
return cc, nil
}

return nil, noClientConnErr
return nil, errNoClientConn
}

func (p *connPool) MarkDead(c *http2.ClientConn) {
Expand All @@ -56,7 +56,7 @@ func (p *connPool) addHostConn(host string, conn net.Conn) error {
defer p.mu.Unlock()

if _, ok := p.conns[host]; ok {
return clientAlreadyConnectedErr
return errClientAlreadyConnected
}

cc, err := p.t.NewClientConn(conn)
Expand Down
21 changes: 13 additions & 8 deletions proto/controlmsg.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ import (
"regexp"
)

const (
HTTPProtocol = "http"
)

// Action represents type of ControlMsg request.
// Action represents type of ControlMessage.
type Action int

// ControlMessage actions.
Expand All @@ -24,6 +20,15 @@ const (
ForwardedHeader = "Forwarded"
)

// Additional protocols, base protocols are net.Dial networks.
//
// Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only), "udp",
// "udp4" (IPv4-only), "udp6" (IPv6-only), "ip", "ip4" (IPv4-only), "ip6" (IPv6-only),
// "unix", "unixgram" and "unixpacket".
const (
HTTPProtocol = "http"
)

// ControlMessage is sent from server to client to establish tunneled connection.
type ControlMessage struct {
Action Action
Expand All @@ -35,7 +40,7 @@ type ControlMessage struct {

var xffRegexp = regexp.MustCompile("(for|by|proto|path)=([^;$]+)")

// NewControlMessage creates control message based on `Forwarded` http header.
// ParseControlMessage creates new ControlMessage based on "Forwarded" http header.
func ParseControlMessage(h http.Header) (*ControlMessage, error) {
v := h.Get(ForwardedHeader)
if v == "" {
Expand All @@ -60,13 +65,13 @@ func ParseControlMessage(h http.Header) (*ControlMessage, error) {
return msg, nil
}

// WriteTo writes ControlMessage to `Forwarded` http header, "by" and "for" parameters
// WriteTo writes ControlMessage to "Forwarded" http header, "by" and "for" parameters
// take form of full IP and port.
//
// If the server receiving proxied requests requires some address-based functionality,
// this parameter MAY instead contain an IP address (and, potentially, a port number)
//
// see https://tools.ietf.org/html/rfc7239.
// See https://tools.ietf.org/html/rfc7239.
func (c *ControlMessage) WriteTo(h http.Header) {
h.Set(ForwardedHeader, fmt.Sprintf("for=%s; by=%s; proto=%s; path=%s",
c.ForwardedFor, c.ForwardedBy, c.Protocol, c.URLPath))
Expand Down
Loading

0 comments on commit 5707fa1

Please sign in to comment.