From cf94f0a59f4f37347329de31574f42e9ade28fe6 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sun, 1 Oct 2017 14:40:48 -0700 Subject: [PATCH 01/24] internal: add testing for a few libraries --- internal/elfs/elfs_test.go | 10 +++++++ internal/middleware/trace_test.go | 33 +++++++++++++++++++++ internal/routecrypto/rsa_test.go | 41 ++++++++++++++++++++++++++ internal/routecrypto/secretbox_test.go | 40 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+) create mode 100644 internal/elfs/elfs_test.go create mode 100644 internal/middleware/trace_test.go create mode 100644 internal/routecrypto/rsa_test.go create mode 100644 internal/routecrypto/secretbox_test.go diff --git a/internal/elfs/elfs_test.go b/internal/elfs/elfs_test.go new file mode 100644 index 0000000..c433477 --- /dev/null +++ b/internal/elfs/elfs_test.go @@ -0,0 +1,10 @@ +package elfs + +import "testing" + +func TestMakeName(t *testing.T) { + n := MakeName() + if len(n) == 0 { + t.Fatalf("MakeName had a zero output") + } +} diff --git a/internal/middleware/trace_test.go b/internal/middleware/trace_test.go new file mode 100644 index 0000000..cc4461a --- /dev/null +++ b/internal/middleware/trace_test.go @@ -0,0 +1,33 @@ +package middleware + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" +) + +func TestTrace(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var executed bool + var handler http.Handler = Trace(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + executed = true + w.WriteHeader(http.StatusOK) + })) + + req, err := http.NewRequest("GET", "/", nil) + if err != nil { + t.Fatalf("error when creating request: %v", err) + } + req = req.WithContext(ctx) + + rw := httptest.NewRecorder() + + handler.ServeHTTP(rw, req) + + if !executed { + t.Fatal("middleware Trace doesn't pass through to underlying handler") + } +} diff --git a/internal/routecrypto/rsa_test.go b/internal/routecrypto/rsa_test.go new file mode 100644 index 0000000..08e742e --- /dev/null +++ b/internal/routecrypto/rsa_test.go @@ -0,0 +1,41 @@ +package routecrypto + +import "testing" + +var ( + rsaPrivKey = []byte(`-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC6C94euSI3GAbszcTVvuBI4ejM/fugqe/uUyXz2bUIGemkADBh +OOkNWXFi/gnYylHRrFKOH06wxhzZWpsBMacmwx6tD7a7nKktcw7HsVFL8is0PPnp +syhWfW+DF6vMDZxkgI3iKrr9/WY/3/qUg7ga17s1JXb3SmQ2sMDTh5I6DQIET4Bo +LwKBgCBG2EmsLiVPCXwN+Mk8IGck7BHKhVpcm955VDDiuKNMuFK4F9ak3tbsKOza +UDC+JhqhB1U7/J8zABM+qVqHBwse1sJMZUEXPuGbIuw4vmEHFA+scAuwkpmRx4gA +/Ghi9eWr1rDlrRFMEF5vs18GObY7Z07GxTx/nZPx7FZ+6FqZAkEA24zob4NMKGUj +efHggZ4DFiIGDEbfbRS6a/w7VicJwI41pwhbGj7KCPZEwXYhnXR3H9UXSrowsm14 +D0Wbsw4gRwJBANjvAbFVBAW8TWxLCgKx7uyHehygEBl5NY2in/8QHMjJpE7fQX5U +qutOL68A6+8P0lrtoz4VJZSnAxwkaifM8QsCQA37iRRm+Qd64OetQrHj+FhiZlrJ +LAT0CUWmADJ5KYX49B2lfNXDrXOsUG9sZ4tHKRGDt51KC/0KjMgq9BGx41MCQF0y +FxOL0s2EtXz/33V4QA9twe9xUBDY4CMts4Eyq3xlscbBBe4IjwrcKuntJ3POkGPS +Xotb9TDONmrANIqlmbECQCD8Uo0bgt8kR5bShqkbW1e5qVNz5w4+tM7Uh+oQMIGB +bC3xLJD4u2NPTwTdqKxxkeicFMKpuiGvX200M/CcoVc= +-----END RSA PRIVATE KEY-----`) +) + +func TestRSA(t *testing.T) { + pk, err := PemToRSAPrivateKey(rsaPrivKey) + if err != nil { + t.Fatalf("can't parse key: %v", err) + } + + pkd := RSAPrivateKeyToPem(pk) + + pk2, err := PemToRSAPrivateKey(pkd) + if err != nil { + t.Fatalf("can't parse key: %v", err) + } + + pkd2 := RSAPrivateKeyToPem(pk2) + + if string(pkd) != string(pkd2) { + t.Fatalf("functions are not 1:1") + } +} diff --git a/internal/routecrypto/secretbox_test.go b/internal/routecrypto/secretbox_test.go new file mode 100644 index 0000000..05e3d52 --- /dev/null +++ b/internal/routecrypto/secretbox_test.go @@ -0,0 +1,40 @@ +package routecrypto + +import "testing" + +func TestSecretBox(t *testing.T) { + var ( + key *[32]byte + sk string + ) + + t.Run("generate key", func(t *testing.T) { + var err error + key, err = GenerateKey() + if err != nil { + t.Fatalf("can't generate key: %v", err) + } + }) + + if key == nil { + t.Fatal("can't continue") + } + + t.Run("show key", func(t *testing.T) { + sk = ShowKey(key) + if len(sk) == 0 { + t.Fatal("expected output to be a nonzero length string") + } + }) + + t.Run("read key", func(t *testing.T) { + readKey, err := ParseKey(sk) + if err != nil { + t.Fatal(err) + } + + if *key != *readKey { + t.Fatal("key did not parse out correctly") + } + }) +} From 6038f2f15163d288d28b12a6b30ccbea7a9eae34 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sun, 1 Oct 2017 22:24:19 -0700 Subject: [PATCH 02/24] mage: test to run tests --- mage.go | 9 +++++++++ proto/client/.#client.go | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 120000 proto/client/.#client.go diff --git a/mage.go b/mage.go index 1f7a0ae..6c50aac 100644 --- a/mage.go +++ b/mage.go @@ -187,8 +187,17 @@ func Package() { } } +// Version is the version as git reports. func Version() { ver, err := gitTag() qod.ANE(err) qod.Printlnf("route-%s", ver) } + +// Test runs all of the functional and unit tests for the project. +func Test() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + shouldWork(ctx, nil, wd, "go", "test", "-race", "-v", "./...") +} diff --git a/proto/client/.#client.go b/proto/client/.#client.go deleted file mode 120000 index 81f4155..0000000 --- a/proto/client/.#client.go +++ /dev/null @@ -1 +0,0 @@ -xena@greedo.xeserv.us.17867:1486865539 \ No newline at end of file From 223b816f888ee29d079085396f9710b3f39bcc96 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Sun, 1 Oct 2017 22:52:22 -0700 Subject: [PATCH 03/24] internal/tun2: forward stream buffered reader and closer to response bodies --- internal/tun2/connection.go | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/internal/tun2/connection.go b/internal/tun2/connection.go index 9db2195..20f05dc 100644 --- a/internal/tun2/connection.go +++ b/internal/tun2/connection.go @@ -2,8 +2,8 @@ package tun2 import ( "bufio" - "bytes" "context" + "io" "io/ioutil" "net" "net/http" @@ -127,7 +127,6 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil { return nil, errors.Wrap(err, ErrCantOpenSessionStream.Error()) } - defer stream.Close() err = req.Write(stream) if err != nil { @@ -147,8 +146,17 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { return nil, errors.Wrap(err, "can't read response body") } - resp.Body = ioutil.NopCloser(bytes.NewBuffer(body)) + resp.Body = &mixedReadCloser{ + Reader: buf, + Closer: stream, + } + resp.ContentLength = int64(len(body)) return resp, nil } + +type mixedReadCloser struct { + io.Reader + io.Closer +} From 64524eefc195abe8578f6396c323f173492020c3 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 06:31:05 -0700 Subject: [PATCH 04/24] tun2: connection: do things better --- internal/tun2/connection.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/internal/tun2/connection.go b/internal/tun2/connection.go index 20f05dc..aa79c81 100644 --- a/internal/tun2/connection.go +++ b/internal/tun2/connection.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "io" - "io/ioutil" "net" "net/http" "time" @@ -139,20 +138,12 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil { return nil, errors.Wrap(err, ErrCantReadResponse.Error()) } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, errors.Wrap(err, "can't read response body") - } resp.Body = &mixedReadCloser{ Reader: buf, Closer: stream, } - resp.ContentLength = int64(len(body)) - return resp, nil } From 99d5acb880180ca548d6e8833bf47d03d06909bb Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 06:34:09 -0700 Subject: [PATCH 05/24] Revert "tun2: connection: do things better" This reverts commit 64524eefc195abe8578f6396c323f173492020c3. re --- internal/tun2/connection.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/tun2/connection.go b/internal/tun2/connection.go index aa79c81..20f05dc 100644 --- a/internal/tun2/connection.go +++ b/internal/tun2/connection.go @@ -4,6 +4,7 @@ import ( "bufio" "context" "io" + "io/ioutil" "net" "net/http" "time" @@ -138,12 +139,20 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil { return nil, errors.Wrap(err, ErrCantReadResponse.Error()) } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "can't read response body") + } resp.Body = &mixedReadCloser{ Reader: buf, Closer: stream, } + resp.ContentLength = int64(len(body)) + return resp, nil } From 88d0a0a619091bccb6e27b6bbde19af4704e195b Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 06:34:21 -0700 Subject: [PATCH 06/24] Revert "internal/tun2: forward stream buffered reader and closer to response bodies" This reverts commit 223b816f888ee29d079085396f9710b3f39bcc96. --- internal/tun2/connection.go | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/internal/tun2/connection.go b/internal/tun2/connection.go index 20f05dc..9db2195 100644 --- a/internal/tun2/connection.go +++ b/internal/tun2/connection.go @@ -2,8 +2,8 @@ package tun2 import ( "bufio" + "bytes" "context" - "io" "io/ioutil" "net" "net/http" @@ -127,6 +127,7 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil { return nil, errors.Wrap(err, ErrCantOpenSessionStream.Error()) } + defer stream.Close() err = req.Write(stream) if err != nil { @@ -146,17 +147,8 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { return nil, errors.Wrap(err, "can't read response body") } - resp.Body = &mixedReadCloser{ - Reader: buf, - Closer: stream, - } - + resp.Body = ioutil.NopCloser(bytes.NewBuffer(body)) resp.ContentLength = int64(len(body)) return resp, nil } - -type mixedReadCloser struct { - io.Reader - io.Closer -} From 0a7f7a4652de43f6dd8f74ffc9bb2efa6af86bd4 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Mon, 2 Oct 2017 20:26:02 -0700 Subject: [PATCH 07/24] plugins/autohttpagent: stub out tests --- plugins/autohttpagent/main_test.go | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 plugins/autohttpagent/main_test.go diff --git a/plugins/autohttpagent/main_test.go b/plugins/autohttpagent/main_test.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/plugins/autohttpagent/main_test.go @@ -0,0 +1,3 @@ +package main + +func main() {} From 5afb3715ccbfa40d39e51c9b2bbbe743ce269cf5 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 13:20:23 -0700 Subject: [PATCH 08/24] tun2: documentation and unit tests --- internal/tun2/backend.go | 71 ++++++++++++ internal/tun2/client.go | 15 ++- internal/tun2/client_test.go | 21 ++++ internal/tun2/server.go | 219 ++++++++++++++--------------------- internal/tun2/server_test.go | 53 +++++++++ 5 files changed, 241 insertions(+), 138 deletions(-) create mode 100644 internal/tun2/client_test.go create mode 100644 internal/tun2/server_test.go diff --git a/internal/tun2/backend.go b/internal/tun2/backend.go index 37af1aa..30fd2e1 100644 --- a/internal/tun2/backend.go +++ b/internal/tun2/backend.go @@ -1,5 +1,7 @@ package tun2 +import "time" + // Backend is the public state of an individual Connection. type Backend struct { ID string @@ -10,3 +12,72 @@ type Backend struct { Host string Usable bool } + +type backendMatcher func(*Connection) bool + +func (s *Server) getBackendsForMatcher(bm backendMatcher) []Backend { + s.connlock.Lock() + defer s.connlock.Unlock() + + var result []Backend + + for _, c := range s.conns { + if !bm(c) { + continue + } + + protocol := "tcp" + if c.isKCP { + protocol = "kcp" + } + + result = append(result, Backend{ + ID: c.id, + Proto: protocol, + User: c.user, + Domain: c.domain, + Phi: float32(c.detector.Phi(time.Now())), + Host: c.conn.RemoteAddr().String(), + Usable: c.usable, + }) + } + + return result +} + +// KillBackend forcibly disconnects a given backend but doesn't offer a way to +// "ban" it from reconnecting. +func (s *Server) KillBackend(id string) error { + s.connlock.Lock() + defer s.connlock.Unlock() + + for _, c := range s.conns { + if c.id == id { + c.cancel() + return nil + } + } + + return ErrNoSuchBackend +} + +// GetBackendsForDomain fetches all backends connected to this server associated +// to a single public domain name. +func (s *Server) GetBackendsForDomain(domain string) []Backend { + return s.getBackendsForMatcher(func(c *Connection) bool { + return c.domain == domain + }) +} + +// GetBackendsForUser fetches all backends connected to this server owned by a +// given user by username. +func (s *Server) GetBackendsForUser(uname string) []Backend { + return s.getBackendsForMatcher(func(c *Connection) bool { + return c.user == uname + }) +} + +// GetAllBackends fetches every backend connected to this server. +func (s *Server) GetAllBackends() []Backend { + return s.getBackendsForMatcher(func(*Connection) bool { return true }) +} diff --git a/internal/tun2/client.go b/internal/tun2/client.go index adb1c8b..8c38c5c 100644 --- a/internal/tun2/client.go +++ b/internal/tun2/client.go @@ -14,10 +14,14 @@ import ( "github.com/xtaci/smux" ) +// Client connects to a remote tun2 server and sets up authentication before routing +// individual HTTP requests to discrete streams that are reverse proxied to the eventual +// backend. type Client struct { cfg *ClientConfig } +// ClientConfig configures client with settings that the user provides. type ClientConfig struct { TLSConfig *tls.Config ConnType string @@ -27,6 +31,7 @@ type ClientConfig struct { BackendURL string } +// NewClient constructs an instance of Client with a given ClientConfig. func NewClient(cfg *ClientConfig) (*Client, error) { if cfg == nil { return nil, errors.New("tun2: client config needed") @@ -39,6 +44,11 @@ func NewClient(cfg *ClientConfig) (*Client, error) { return c, nil } +// Connect dials the remote server and negotiates a client session with its +// configured server address. This will then continuously proxy incoming HTTP +// requests to the backend HTTP server. +// +// This is a blocking function. func (c *Client) Connect() error { return c.connect(c.cfg.ServerAddr) } @@ -117,15 +127,12 @@ func (c *Client) connect(serverAddr string) error { return nil } +// smuxListener wraps a smux session as a net.Listener. type smuxListener struct { conn net.Conn session *smux.Session } -var ( - _ net.Listener = &smuxListener{} // interface check -) - func (sl *smuxListener) Accept() (net.Conn, error) { return sl.session.AcceptStream() } diff --git a/internal/tun2/client_test.go b/internal/tun2/client_test.go new file mode 100644 index 0000000..d3127a7 --- /dev/null +++ b/internal/tun2/client_test.go @@ -0,0 +1,21 @@ +package tun2 + +import ( + "net" + "testing" +) + +func TestNewClientNullConfig(t *testing.T) { + _, err := NewClient(nil) + if err == nil { + t.Fatalf("expected NewClient(nil) to fail, got non-failure") + } +} + +func TestSmuxListenerIsNetListener(t *testing.T) { + var sl interface{} = &smuxListener{} + _, ok := sl.(net.Listener) + if !ok { + t.Fatalf("smuxListener does not implement net.Listener") + } +} diff --git a/internal/tun2/server.go b/internal/tun2/server.go index 783d952..332ab4b 100644 --- a/internal/tun2/server.go +++ b/internal/tun2/server.go @@ -30,6 +30,40 @@ var ( ErrCantRemoveWhatDoesntExist = errors.New("tun2: this connection does not exist, cannot remove it") ) +// gen502Page creates the page that is shown when a backend is not connected to a given route. +func gen502Page(req *http.Request) *http.Response { + template := `no backends connected

no backends connected

Please ensure a backend is running for ${HOST}. This is request ID ${REQ_ID}.

` + + resbody := []byte(os.Expand(template, func(in string) string { + switch in { + case "HOST": + return req.Host + case "REQ_ID": + return req.Header.Get("X-Request-Id") + } + + return "" + })) + reshdr := req.Header + reshdr.Set("Content-Type", "text/html; charset=utf-8") + + resp := &http.Response{ + Status: fmt.Sprintf("%d Bad Gateway", http.StatusBadGateway), + StatusCode: http.StatusBadGateway, + Body: ioutil.NopCloser(bytes.NewBuffer(resbody)), + + Proto: req.Proto, + ProtoMajor: req.ProtoMajor, + ProtoMinor: req.ProtoMinor, + Header: reshdr, + ContentLength: int64(len(resbody)), + Close: true, + Request: req, + } + + return resp +} + // ServerConfig ... type ServerConfig struct { TCPAddr string @@ -81,66 +115,29 @@ func NewServer(cfg *ServerConfig) (*Server, error) { return server, nil } -type backendMatcher func(*Connection) bool +// Listen passes this Server a given net.Listener to accept backend connections. +func (s *Server) Listen(l net.Listener, isKCP bool) { + ctx := context.Background() -func (s *Server) getBackendsForMatcher(bm backendMatcher) []Backend { - s.connlock.Lock() - defer s.connlock.Unlock() - - var result []Backend - - for _, c := range s.conns { - if !bm(c) { + for { + conn, err := l.Accept() + if err != nil { + ln.Error(ctx, err, ln.F{ + "addr": l.Addr().String(), + "network": l.Addr().Network(), + }) continue } - protocol := "tcp" - if c.isKCP { - protocol = "kcp" - } - - result = append(result, Backend{ - ID: c.id, - Proto: protocol, - User: c.user, - Domain: c.domain, - Phi: float32(c.detector.Phi(time.Now())), - Host: c.conn.RemoteAddr().String(), - Usable: c.usable, + ln.Log(ctx, ln.F{ + "action": "new_client", + "network": conn.RemoteAddr().Network(), + "addr": conn.RemoteAddr(), + "list": conn.LocalAddr(), }) + + go s.HandleConn(conn, isKCP) } - - return result -} - -func (s *Server) KillBackend(id string) error { - s.connlock.Lock() - defer s.connlock.Unlock() - - for _, c := range s.conns { - if c.id == id { - c.cancel() - return nil - } - } - - return ErrNoSuchBackend -} - -func (s *Server) GetBackendsForDomain(domain string) []Backend { - return s.getBackendsForMatcher(func(c *Connection) bool { - return c.domain == domain - }) -} - -func (s *Server) GetBackendsForUser(uname string) []Backend { - return s.getBackendsForMatcher(func(c *Connection) bool { - return c.user == uname - }) -} - -func (s *Server) GetAllBackends() []Backend { - return s.getBackendsForMatcher(func(*Connection) bool { return true }) } // ListenAndServe starts the backend TCP/KCP listeners and relays backend @@ -248,67 +245,62 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + f := ln.F{ + "local": c.LocalAddr().String(), + "remote": c.RemoteAddr().String(), + } + session, err := smux.Server(c, s.cfg.SmuxConf) if err != nil { - ln.Error(ctx, err, ln.F{ - "action": "session_failure", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) - - c.Close() + ln.Error(ctx, err, f, ln.Action("establish server side of smux")) return } defer session.Close() + f["stage"] = "smux_setup" + controlStream, err := session.OpenStream() if err != nil { - ln.Error(ctx, err, ln.F{ - "action": "control_stream_failure", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) + ln.Error(ctx, err, f, ln.Action("opening control stream")) return } defer controlStream.Close() + f["stage"] = "control_stream_open" + csd := json.NewDecoder(controlStream) auth := &Auth{} err = csd.Decode(auth) if err != nil { - ln.Error(ctx, err, ln.F{ - "action": "control_stream_auth_decoding_failure", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) + ln.Error(ctx, err, f, ln.Action("decode control stream authenication message")) return } + f["stage"] = "checking_domain" + routeUser, err := s.cfg.Storage.HasRoute(auth.Domain) if err != nil { - ln.Error(ctx, err, ln.F{ - "action": "nosuch_domain", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) + ln.Error(ctx, err, f, ln.Action("no such domain when checking client auth")) return } + f["route_user"] = routeUser + f["stage"] = "checking_token" + tokenUser, scopes, err := s.cfg.Storage.HasToken(auth.Token) if err != nil { - ln.Error(ctx, err, ln.F{ - "action": "nosuch_token", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) + ln.Error(ctx, err, f, ln.Action("no such token exists or other token error")) return } + f["token_user"] = tokenUser + f["stage"] = "checking_token_scopes" + ok := false for _, sc := range scopes { if sc == "connect" { @@ -318,19 +310,15 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { } if !ok { - ln.Error(ctx, ErrAuthMismatch, ln.F{ - "action": "token_not_authorized", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) + ln.Error(ctx, ErrAuthMismatch, f, ln.Action("token not authorized to connect")) + + return } + f["stage"] = "user_verification" + if routeUser != tokenUser { - ln.Error(ctx, ErrAuthMismatch, ln.F{ - "action": "auth_mismatch", - "local": c.LocalAddr().String(), - "remote": c.RemoteAddr().String(), - }) + ln.Error(ctx, ErrAuthMismatch, f, ln.Action("auth mismatch")) return } @@ -353,10 +341,9 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { } }() - ln.Log(ctx, ln.F{ - "action": "backend_connected", - }, connection.F()) + ln.Log(ctx, connection, ln.Action("backend successfully connected")) + // TODO: put these lines in a function? s.connlock.Lock() s.conns[c] = connection s.connlock.Unlock() @@ -376,7 +363,7 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { conns = append(conns, connection) s.domains.Set(auth.Domain, conns) - connection.usable = true + connection.usable = true // XXX set this to true once health checks pass? ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() @@ -411,9 +398,8 @@ func (s *Server) RemoveConn(ctx context.Context, connection *Connection) { if ok { conns, ok = val.([]*Connection) if !ok { - ln.Error(ctx, ErrCantRemoveWhatDoesntExist, connection.F(), ln.F{ - "action": "looking_up_for_disconnect_removal", - }) + ln.Error(ctx, ErrCantRemoveWhatDoesntExist, connection, ln.Action("looking up for disconnect removal")) + return } } @@ -431,42 +417,7 @@ func (s *Server) RemoveConn(ctx context.Context, connection *Connection) { s.domains.Remove(auth.Domain) } - ln.Log(ctx, connection.F(), ln.F{ - "action": "client_disconnecting", - }) -} - -func gen502Page(req *http.Request) *http.Response { - template := `no backends connected

no backends connected

Please ensure a backend is running for ${HOST}. This is request ID ${REQ_ID}.

` - - resbody := []byte(os.Expand(template, func(in string) string { - switch in { - case "HOST": - return req.Host - case "REQ_ID": - return req.Header.Get("X-Request-Id") - } - - return "" - })) - reshdr := req.Header - reshdr.Set("Content-Type", "text/html; charset=utf-8") - - resp := &http.Response{ - Status: fmt.Sprintf("%d Bad Gateway", http.StatusBadGateway), - StatusCode: http.StatusBadGateway, - Body: ioutil.NopCloser(bytes.NewBuffer(resbody)), - - Proto: req.Proto, - ProtoMajor: req.ProtoMajor, - ProtoMinor: req.ProtoMinor, - Header: reshdr, - ContentLength: int64(len(resbody)), - Close: true, - Request: req, - } - - return resp + ln.Log(ctx, connection, ln.Action("backend disconnect")) } // RoundTrip sends a HTTP request to a backend and then returns its response. diff --git a/internal/tun2/server_test.go b/internal/tun2/server_test.go new file mode 100644 index 0000000..3f37c4e --- /dev/null +++ b/internal/tun2/server_test.go @@ -0,0 +1,53 @@ +package tun2 + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/Xe/uuid" +) + +func TestNewServerNullConfig(t *testing.T) { + _, err := NewServer(nil) + if err == nil { + t.Fatalf("expected NewServer(nil) to fail, got non-failure") + } +} + +func TestGen502Page(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + req, err := http.NewRequest("GET", "http://cetacean.club", nil) + if err != nil { + t.Fatal(err) + } + + substring := uuid.New() + + req = req.WithContext(ctx) + req.Header.Add("X-Request-Id", substring) + req.Host = "cetacean.club" + + resp := gen502Page(req) + if resp == nil { + t.Fatalf("expected response to be non-nil") + } + + if resp.Body != nil { + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + if !strings.Contains(string(data), substring) { + fmt.Println(string(data)) + t.Fatalf("502 page did not contain needed substring %q", substring) + } + } +} From 28859427b6a71e1b4d2804cc218dc65495c69de9 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 15:20:38 -0700 Subject: [PATCH 09/24] plugins/autohttpagent: that was a bad idea --- plugins/autohttpagent/main_test.go | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 plugins/autohttpagent/main_test.go diff --git a/plugins/autohttpagent/main_test.go b/plugins/autohttpagent/main_test.go deleted file mode 100644 index 38dd16d..0000000 --- a/plugins/autohttpagent/main_test.go +++ /dev/null @@ -1,3 +0,0 @@ -package main - -func main() {} From df641cf1c5d7da4d903c7e73c5e3683f6b69d681 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 15:22:21 -0700 Subject: [PATCH 10/24] update gitignores --- cmd/route-cli/.gitignore | 2 +- cmd/route-httpagent/.gitignore | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/route-cli/.gitignore b/cmd/route-cli/.gitignore index 6f36346..41fbb63 100644 --- a/cmd/route-cli/.gitignore +++ b/cmd/route-cli/.gitignore @@ -1 +1 @@ -route +route-cli diff --git a/cmd/route-httpagent/.gitignore b/cmd/route-httpagent/.gitignore index 0380c39..f22d7b4 100644 --- a/cmd/route-httpagent/.gitignore +++ b/cmd/route-httpagent/.gitignore @@ -1 +1 @@ -route-httpagent \ No newline at end of file +route-httpagent From a47fd75c5f40ef79efc6cb017395c096c2f9d2d9 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 15:22:55 -0700 Subject: [PATCH 11/24] Revert "plugins/autohttpagent: that was a bad idea" removing this was a worse idea This reverts commit 28859427b6a71e1b4d2804cc218dc65495c69de9. --- plugins/autohttpagent/main_test.go | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 plugins/autohttpagent/main_test.go diff --git a/plugins/autohttpagent/main_test.go b/plugins/autohttpagent/main_test.go new file mode 100644 index 0000000..38dd16d --- /dev/null +++ b/plugins/autohttpagent/main_test.go @@ -0,0 +1,3 @@ +package main + +func main() {} From 59a3f45150d57bc22288079dc0d40d220e34b342 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Tue, 3 Oct 2017 23:43:31 -0700 Subject: [PATCH 12/24] tun2: some experimenting on the core --- internal/tun2/client.go | 13 +- internal/tun2/server.go | 338 ++++++++++++++++------------------ internal/tun2/server_test.go | 176 ++++++++++++++++++ internal/tun2/storage_test.go | 99 ++++++++++ 4 files changed, 446 insertions(+), 180 deletions(-) create mode 100644 internal/tun2/storage_test.go diff --git a/internal/tun2/client.go b/internal/tun2/client.go index 8c38c5c..635a770 100644 --- a/internal/tun2/client.go +++ b/internal/tun2/client.go @@ -1,6 +1,7 @@ package tun2 import ( + "context" "crypto/tls" "encoding/json" "errors" @@ -29,6 +30,9 @@ type ClientConfig struct { Token string Domain string BackendURL string + + // internal use only + forceTCPClear bool } // NewClient constructs an instance of Client with a given ClientConfig. @@ -49,7 +53,7 @@ func NewClient(cfg *ClientConfig) (*Client, error) { // requests to the backend HTTP server. // // This is a blocking function. -func (c *Client) Connect() error { +func (c *Client) Connect(ctx context.Context) error { return c.connect(c.cfg.ServerAddr) } @@ -67,7 +71,12 @@ func (c *Client) connect(serverAddr string) error { switch c.cfg.ConnType { case "tcp": - conn, err = tls.Dial("tcp", serverAddr, c.cfg.TLSConfig) + if c.cfg.forceTCPClear { + conn, err = net.Dial("tcp", serverAddr) + } else { + conn, err = tls.Dial("tcp", serverAddr, c.cfg.TLSConfig) + } + if err != nil { return err } diff --git a/internal/tun2/server.go b/internal/tun2/server.go index 332ab4b..3b92180 100644 --- a/internal/tun2/server.go +++ b/internal/tun2/server.go @@ -3,10 +3,10 @@ package tun2 import ( "bytes" "context" - "crypto/tls" "encoding/json" "errors" "fmt" + "io" "io/ioutil" "math/rand" "net" @@ -17,9 +17,9 @@ import ( "github.com/Xe/ln" failure "github.com/dgryski/go-failure" + "github.com/kr/pretty" "github.com/mtneug/pkg/ulid" cmap "github.com/streamrail/concurrent-map" - kcp "github.com/xtaci/kcp-go" "github.com/xtaci/smux" ) @@ -66,10 +66,6 @@ func gen502Page(req *http.Request) *http.Response { // ServerConfig ... type ServerConfig struct { - TCPAddr string - KCPAddr string - TLSConfig *tls.Config - SmuxConf *smux.Config Storage Storage } @@ -83,7 +79,9 @@ type Storage interface { // Server routes frontend HTTP traffic to backend TCP traffic. type Server struct { - cfg *ServerConfig + cfg *ServerConfig + ctx context.Context + cancel context.CancelFunc connlock sync.Mutex conns map[net.Conn]*Connection @@ -100,146 +98,174 @@ func NewServer(cfg *ServerConfig) (*Server, error) { if cfg.SmuxConf == nil { cfg.SmuxConf = smux.DefaultConfig() + + cfg.SmuxConf.KeepAliveInterval = time.Second + cfg.SmuxConf.KeepAliveTimeout = 15 * time.Second } - cfg.SmuxConf.KeepAliveInterval = time.Second - cfg.SmuxConf.KeepAliveTimeout = 15 * time.Second + ctx, cancel := context.WithCancel(context.Background()) server := &Server{ cfg: cfg, conns: map[net.Conn]*Connection{}, domains: cmap.New(), + ctx: ctx, + cancel: cancel, } + go server.phiDetectionLoop(ctx) + return server, nil } +// Close stops the background tasks for this Server. +func (s *Server) Close() { + s.cancel() +} + +// Wait blocks until the server context is cancelled. +func (s *Server) Wait() { + for { + select { + case <-s.ctx.Done(): + return + } + } +} + // Listen passes this Server a given net.Listener to accept backend connections. func (s *Server) Listen(l net.Listener, isKCP bool) { ctx := context.Background() + f := ln.F{ + "listener_addr": l.Addr(), + "listener_network": l.Addr().Network(), + } + for { conn, err := l.Accept() if err != nil { - ln.Error(ctx, err, ln.F{ - "addr": l.Addr().String(), - "network": l.Addr().Network(), - }) + ln.Error(ctx, err, f, ln.Action("accept connection")) continue } - ln.Log(ctx, ln.F{ - "action": "new_client", - "network": conn.RemoteAddr().Network(), - "addr": conn.RemoteAddr(), - "list": conn.LocalAddr(), + ln.Log(ctx, f, ln.Action("new backend client connected"), ln.F{ + "conn_addr": conn.RemoteAddr(), + "conn_network": conn.RemoteAddr().Network(), }) go s.HandleConn(conn, isKCP) } } -// ListenAndServe starts the backend TCP/KCP listeners and relays backend -// traffic to and from them. -func (s *Server) ListenAndServe() error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - ln.Log(ctx, ln.F{ - "action": "listen_and_serve_called", - }) - - if s.cfg.TCPAddr != "" { - go func() { - l, err := tls.Listen("tcp", s.cfg.TCPAddr, s.cfg.TLSConfig) - if err != nil { - panic(err) - } - - ln.Log(ctx, ln.F{ - "action": "tcp+tls_listening", - "addr": l.Addr(), - }) - - for { - conn, err := l.Accept() - if err != nil { - ln.Error(ctx, err, ln.F{"kind": "tcp", "addr": l.Addr().String()}) - continue - } - - ln.Log(ctx, ln.F{ - "action": "new_client", - "kcp": false, - "addr": conn.RemoteAddr(), - }) - - go s.HandleConn(conn, false) - } - }() - } - - if s.cfg.KCPAddr != "" { - go func() { - l, err := kcp.Listen(s.cfg.KCPAddr) - if err != nil { - panic(err) - } - - ln.Log(ctx, ln.F{ - "action": "kcp+tls_listening", - "addr": l.Addr(), - }) - - for { - conn, err := l.Accept() - if err != nil { - ln.Error(ctx, err, ln.F{"kind": "kcp", "addr": l.Addr().String()}) - } - - ln.Log(ctx, ln.F{ - "action": "new_client", - "kcp": true, - "addr": conn.RemoteAddr(), - }) - - tc := tls.Server(conn, s.cfg.TLSConfig) - - go s.HandleConn(tc, true) - } - }() - } - - // XXX experimental, might get rid of this inside this process - go func() { - for { - time.Sleep(time.Second) +// phiDetectionLoop is an infinite loop that will run the [phi accrual failure detector] +// for each of the backends connected to the Server. This is fairly experimental and +// may be removed. +// +// [phi accrual failure detector]: https://dspace.jaist.ac.jp/dspace/handle/10119/4784 +func (s *Server) phiDetectionLoop(ctx context.Context) { + t := time.NewTicker(5 * time.Second) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: now := time.Now() s.connlock.Lock() for _, c := range s.conns { failureChance := c.detector.Phi(now) + const thresh = 0.9 // the threshold for phi failure detection causing logs - if failureChance > 0.8 { - ln.Log(ctx, c.F(), ln.F{ - "action": "phi_failure_detection", - "value": failureChance, + if failureChance > thresh { + ln.Log(ctx, c, ln.Action("phi failure detection"), ln.F{ + "value": failureChance, + "threshold": thresh, }) } } s.connlock.Unlock() } - }() + } +} - return nil +// backendAuthv1 runs a simple backend authentication check. It expects the +// client to write a json-encoded instance of Auth. This is then checked +// for token validity and domain matching. +// +// This returns the user that was authenticated and the domain they identified +// with. +func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth, error) { + f := ln.F{ + "action": "backend authentication", + "backend_auth_version": 1, + } + + f["stage"] = "json decoding" + ln.Log(ctx, f) + + d := json.NewDecoder(st) + var auth Auth + err := d.Decode(&auth) + if err != nil { + ln.Error(ctx, err, f) + return "", nil, err + } + + f["stage"] = "checking domain" + ln.Log(ctx, f) + + pretty.Println(s.cfg.Storage) + routeUser, err := s.cfg.Storage.HasRoute(auth.Domain) + if err != nil { + ln.Error(ctx, err, f) + return "", nil, err + } + + f["route_user"] = routeUser + f["stage"] = "checking token" + ln.Log(ctx, f) + + tokenUser, scopes, err := s.cfg.Storage.HasToken(auth.Token) + if err != nil { + ln.Error(ctx, err, f) + return "", nil, err + } + + f["token_user"] = tokenUser + f["stage"] = "checking token scopes" + ln.Log(ctx, f) + + ok := false + for _, sc := range scopes { + if sc == "connect" { + ok = true + break + } + } + + if !ok { + ln.Error(ctx, ErrAuthMismatch, f) + return "", nil, ErrAuthMismatch + } + + f["stage"] = "user verification" + ln.Log(ctx, f) + + if routeUser != tokenUser { + ln.Error(ctx, ErrAuthMismatch, f) + return "", nil, ErrAuthMismatch + } + + return routeUser, &auth, nil } // HandleConn starts up the needed mechanisms to relay HTTP traffic to/from // the currently connected backend. func (s *Server) HandleConn(c net.Conn, isKCP bool) { - // XXX TODO clean this up it's really ugly. defer c.Close() ctx, cancel := context.WithCancel(context.Background()) @@ -258,8 +284,6 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { } defer session.Close() - f["stage"] = "smux_setup" - controlStream, err := session.OpenStream() if err != nil { ln.Error(ctx, err, f, ln.Action("opening control stream")) @@ -268,58 +292,8 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { } defer controlStream.Close() - f["stage"] = "control_stream_open" - - csd := json.NewDecoder(controlStream) - auth := &Auth{} - err = csd.Decode(auth) + user, auth, err := s.backendAuthv1(ctx, controlStream) if err != nil { - ln.Error(ctx, err, f, ln.Action("decode control stream authenication message")) - - return - } - - f["stage"] = "checking_domain" - - routeUser, err := s.cfg.Storage.HasRoute(auth.Domain) - if err != nil { - ln.Error(ctx, err, f, ln.Action("no such domain when checking client auth")) - - return - } - - f["route_user"] = routeUser - f["stage"] = "checking_token" - - tokenUser, scopes, err := s.cfg.Storage.HasToken(auth.Token) - if err != nil { - ln.Error(ctx, err, f, ln.Action("no such token exists or other token error")) - - return - } - - f["token_user"] = tokenUser - f["stage"] = "checking_token_scopes" - - ok := false - for _, sc := range scopes { - if sc == "connect" { - ok = true - break - } - } - - if !ok { - ln.Error(ctx, ErrAuthMismatch, f, ln.Action("token not authorized to connect")) - - return - } - - f["stage"] = "user_verification" - - if routeUser != tokenUser { - ln.Error(ctx, ErrAuthMismatch, f, ln.Action("auth mismatch")) - return } @@ -328,7 +302,7 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { conn: c, isKCP: isKCP, session: session, - user: tokenUser, + user: user, domain: auth.Domain, cf: cancel, detector: failure.New(15, 1), @@ -343,26 +317,8 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { ln.Log(ctx, connection, ln.Action("backend successfully connected")) - // TODO: put these lines in a function? - s.connlock.Lock() - s.conns[c] = connection - s.connlock.Unlock() + s.addConn(ctx, connection) - var conns []*Connection - - val, ok := s.domains.Get(auth.Domain) - if ok { - conns, ok = val.([]*Connection) - if !ok { - conns = nil - - s.domains.Remove(auth.Domain) - } - } - - conns = append(conns, connection) - - s.domains.Set(auth.Domain, conns) connection.usable = true // XXX set this to true once health checks pass? ticker := time.NewTicker(5 * time.Second) @@ -375,8 +331,13 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { if err != nil { connection.cancel() } + case <-s.ctx.Done(): + s.removeConn(ctx, connection) + connection.Close() + + return case <-ctx.Done(): - s.RemoveConn(ctx, connection) + s.removeConn(ctx, connection) connection.Close() return @@ -384,8 +345,31 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { } } -// RemoveConn removes a connection. -func (s *Server) RemoveConn(ctx context.Context, connection *Connection) { +// addConn adds a connection to the pool of backend connections. +func (s *Server) addConn(ctx context.Context, connection *Connection) { + s.connlock.Lock() + s.conns[connection.conn] = connection + s.connlock.Unlock() + + var conns []*Connection + + val, ok := s.domains.Get(connection.domain) + if ok { + conns, ok = val.([]*Connection) + if !ok { + conns = nil + + s.domains.Remove(connection.domain) + } + } + + conns = append(conns, connection) + + s.domains.Set(connection.domain, conns) +} + +// removeConn removes a connection from pool of backend connections. +func (s *Server) removeConn(ctx context.Context, connection *Connection) { s.connlock.Lock() delete(s.conns, connection.conn) s.connlock.Unlock() @@ -416,8 +400,6 @@ func (s *Server) RemoveConn(ctx context.Context, connection *Connection) { } else { s.domains.Remove(auth.Domain) } - - ln.Log(ctx, connection, ln.Action("backend disconnect")) } // RoundTrip sends a HTTP request to a backend and then returns its response. diff --git a/internal/tun2/server_test.go b/internal/tun2/server_test.go index 3f37c4e..0ead668 100644 --- a/internal/tun2/server_test.go +++ b/internal/tun2/server_test.go @@ -1,16 +1,31 @@ package tun2 import ( + "bytes" "context" + "encoding/json" "fmt" "io/ioutil" + "net" "net/http" + "net/http/httptest" + "os" "strings" "testing" + "time" "github.com/Xe/uuid" ) +// testing constants +const ( + user = "shachi" + token = "orcaz r kewl" + noPermToken = "aw heck" + otherUserToken = "even more heck" + domain = "cetacean.club" +) + func TestNewServerNullConfig(t *testing.T) { _, err := NewServer(nil) if err == nil { @@ -51,3 +66,164 @@ func TestGen502Page(t *testing.T) { } } } + +func TestBackendAuthV1(t *testing.T) { + st := MockStorage() + + s, err := NewServer(&ServerConfig{ + Storage: st, + }) + if err != nil { + t.Fatal(err) + } + + st.AddRoute(domain, user) + st.AddToken(token, user, []string{"connect"}) + st.AddToken(noPermToken, user, nil) + st.AddToken(otherUserToken, "cadey", []string{"connect"}) + + cases := []struct { + name string + auth Auth + wantErr bool + }{ + { + name: "basic everything should work", + auth: Auth{ + Token: token, + Domain: domain, + }, + wantErr: false, + }, + { + name: "invalid domain", + auth: Auth{ + Token: token, + Domain: "aw.heck", + }, + wantErr: true, + }, + { + name: "invalid token", + auth: Auth{ + Token: "asdfwtweg", + Domain: domain, + }, + wantErr: true, + }, + { + name: "invalid token scopes", + auth: Auth{ + Token: noPermToken, + Domain: domain, + }, + wantErr: true, + }, + { + name: "user token doesn't match domain owner", + auth: Auth{ + Token: otherUserToken, + Domain: domain, + }, + wantErr: true, + }, + } + + for _, cs := range cases { + t.Run(cs.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + data, err := json.Marshal(cs.auth) + if err != nil { + t.Fatal(err) + } + + _, _, err = s.backendAuthv1(ctx, bytes.NewBuffer(data)) + + if cs.wantErr && err == nil { + t.Fatalf("auth did not err as expected") + } + + if !cs.wantErr && err != nil { + t.Fatalf("unexpected auth err: %v", err) + } + }) + } +} + +func TestBackendRouting(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + st := MockStorage() + + st.AddRoute(domain, user) + st.AddToken(token, user, []string{"connect"}) + + s, err := NewServer(&ServerConfig{ + Storage: st, + }) + if err != nil { + t.Fatal(err) + } + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + + go s.Listen(l, false) + + cases := []struct { + name string + should200 bool + handler http.HandlerFunc + }{ + { + name: "200 everything's okay", + should200: true, + handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "HTTP 200, everything is okay :)", http.StatusOK) + }), + }, + } + + for _, cs := range cases { + t.Run(cs.name, func(t *testing.T) { + ts := httptest.NewServer(cs.handler) + defer ts.Close() + + cc := &ClientConfig{ + ConnType: "tcp", + ServerAddr: l.Addr().String(), + Token: token, + BackendURL: ts.URL, + } + + c, err := NewClient(cc) + if err != nil { + t.Fatal(err) + } + + go c.Connect(ctx) // + + time.Sleep(5 * time.Second) + + req, err := http.NewRequest("GET", "http://cetacean.club/", nil) + if err != nil { + t.Fatal(err) + } + + resp, err := s.RoundTrip(req) + if err != nil { + t.Fatalf("error in doing round trip: %v", err) + } + + if cs.should200 && resp.StatusCode != http.StatusOK { + resp.Write(os.Stdout) + t.Fatalf("got status %d instead of StatusOK", resp.StatusCode) + } + }) + } +} diff --git a/internal/tun2/storage_test.go b/internal/tun2/storage_test.go new file mode 100644 index 0000000..f7e068f --- /dev/null +++ b/internal/tun2/storage_test.go @@ -0,0 +1,99 @@ +package tun2 + +import ( + "errors" + "sync" + "testing" +) + +func MockStorage() *mockStorage { + return &mockStorage{ + tokens: make(map[string]mockToken), + domains: make(map[string]string), + } +} + +type mockToken struct { + user string + scopes []string +} + +// mockStorage is a simple mock of the Storage interface suitable for testing. +type mockStorage struct { + sync.Mutex + tokens map[string]mockToken + domains map[string]string +} + +func (ms *mockStorage) AddToken(token, user string, scopes []string) { + ms.Lock() + defer ms.Unlock() + + ms.tokens[token] = mockToken{user: user, scopes: scopes} +} + +func (ms *mockStorage) AddRoute(domain, user string) { + ms.Lock() + defer ms.Unlock() + + ms.domains[domain] = user +} + +func (ms *mockStorage) HasToken(token string) (string, []string, error) { + ms.Lock() + defer ms.Unlock() + + tok, ok := ms.tokens[token] + if !ok { + return "", nil, errors.New("no such token") + } + + return tok.user, tok.scopes, nil +} + +func (ms *mockStorage) HasRoute(domain string) (string, error) { + ms.Lock() + defer ms.Unlock() + + user, ok := ms.domains[domain] + if !ok { + return "", nil + } + + return user, nil +} + +func TestMockStorage(t *testing.T) { + ms := MockStorage() + + t.Run("token", func(t *testing.T) { + ms.AddToken(token, user, []string{"connect"}) + + us, sc, err := ms.HasToken(token) + if err != nil { + t.Fatal(err) + } + + if us != user { + t.Fatalf("username was %q, expected %q", us, user) + } + + if sc[0] != "connect" { + t.Fatalf("token expected to only have one scope, connect") + } + }) + + t.Run("domain", func(t *testing.T) { + ms.AddRoute(domain, user) + + us, err := ms.HasRoute(domain) + if err != nil { + t.Fatal(err) + } + + if us != user { + t.Fatalf("username was %q, expected %q", us, user) + } + }) + +} From c95a8c543450ac7e08ff891d63aef2a9ef1286d2 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 00:00:16 -0700 Subject: [PATCH 13/24] adjust for tun2 api changes --- cmd/route-httpagent/main.go | 2 +- internal/server/server.go | 71 +++++++++++++++++++++++++++++++++---- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/cmd/route-httpagent/main.go b/cmd/route-httpagent/main.go index 12feafa..f6acac3 100644 --- a/cmd/route-httpagent/main.go +++ b/cmd/route-httpagent/main.go @@ -35,7 +35,7 @@ func main() { client, _ := tun2.NewClient(cfg) for { - err := client.Connect() + err := client.Connect(context.Background()) if err != nil { ln.Error(context.Background(), err, ln.Action("client connection failed")) } diff --git a/internal/server/server.go b/internal/server/server.go index 6cbfbd4..bec338c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,8 +11,11 @@ import ( "git.xeserv.us/xena/route/internal/database" "git.xeserv.us/xena/route/internal/tun2" proto "git.xeserv.us/xena/route/proto" + "github.com/Xe/ln" "github.com/mtneug/pkg/ulid" + kcp "github.com/xtaci/kcp-go" "golang.org/x/crypto/acme/autocert" + "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -46,6 +49,59 @@ type Config struct { CertKey *[32]byte } +func (s *Server) listenTCP(ctx context.Context, addr string, tcfg *tls.Config) { + l, err := tls.Listen("tcp", addr, tcfg) + if err != nil { + panic(err) + } + + ln.Log(ctx, ln.Action("tcp+tls listening"), ln.F{"addr": l.Addr()}) + + for { + conn, err := l.Accept() + if err != nil { + ln.Error(ctx, err, ln.Action("accept backend client socket")) + } + + ln.Log(ctx, ln.F{ + "action": "new backend client", + "addr": conn.RemoteAddr(), + "network": conn.RemoteAddr().Network(), + }) + + go s.ts.HandleConn(conn, false) + } +} + +func (s *Server) listenKCP(ctx context.Context, addr string, tcfg *tls.Config) { + l, err := kcp.Listen(addr) + if err != nil { + panic(err) + } + + ln.Log(ctx, ln.F{ + "action": "kcp+tls listening", + "addr": l.Addr(), + }) + + for { + conn, err := l.Accept() + if err != nil { + ln.Error(ctx, err, ln.F{"kind": "kcp", "addr": l.Addr().String()}) + } + + ln.Log(ctx, ln.F{ + "action": "new_client", + "network": conn.RemoteAddr().Network(), + "addr": conn.RemoteAddr(), + }) + + tc := tls.Server(conn, tcfg) + + go s.ts.HandleConn(tc, true) + } +} + // New creates a new Server func New(cfg Config) (*Server, error) { if cfg.CertKey == nil { @@ -65,11 +121,6 @@ func New(cfg Config) (*Server, error) { } tcfg := &tun2.ServerConfig{ - TCPAddr: cfg.BackendTCPAddr, - KCPAddr: cfg.BackendKCPAddr, - TLSConfig: &tls.Config{ - GetCertificate: m.GetCertificate, - }, Storage: &storageWrapper{ Storage: db, }, @@ -79,6 +130,7 @@ func New(cfg Config) (*Server, error) { if err != nil { return nil, err } + s := &Server{ cfg: &cfg, db: db, @@ -87,8 +139,13 @@ func New(cfg Config) (*Server, error) { Manager: m, } - s.ts = ts - go ts.ListenAndServe() + s.listenKCP(context.Background(), cfg.BackendKCPAddr, &tls.Config{ + GetCertificate: m.GetCertificate, + }) + + s.listenTCP(context.Background(), cfg.BackendTCPAddr, &tls.Config{ + GetCertificate: m.GetCertificate, + }) gs := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{ GetCertificate: m.GetCertificate, From b0828f4458eee8e5eb32c4491057046df0bf8957 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 00:03:27 -0700 Subject: [PATCH 14/24] tun2: go these --- internal/server/server.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index bec338c..31a4c1e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -55,6 +55,11 @@ func (s *Server) listenTCP(ctx context.Context, addr string, tcfg *tls.Config) { panic(err) } + ln.Log(ctx, ln.F{ + "action": "tcp+tls listening", + "addr": l.Addr(), + }) + ln.Log(ctx, ln.Action("tcp+tls listening"), ln.F{"addr": l.Addr()}) for { @@ -139,11 +144,11 @@ func New(cfg Config) (*Server, error) { Manager: m, } - s.listenKCP(context.Background(), cfg.BackendKCPAddr, &tls.Config{ + go s.listenKCP(context.Background(), cfg.BackendKCPAddr, &tls.Config{ GetCertificate: m.GetCertificate, }) - s.listenTCP(context.Background(), cfg.BackendTCPAddr, &tls.Config{ + go s.listenTCP(context.Background(), cfg.BackendTCPAddr, &tls.Config{ GetCertificate: m.GetCertificate, }) From 443d8ebb2c73b9739c729ff40401aeff83d56016 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 00:06:17 -0700 Subject: [PATCH 15/24] remove this debugging dep --- internal/tun2/server.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/tun2/server.go b/internal/tun2/server.go index 3b92180..c131d3c 100644 --- a/internal/tun2/server.go +++ b/internal/tun2/server.go @@ -17,7 +17,6 @@ import ( "github.com/Xe/ln" failure "github.com/dgryski/go-failure" - "github.com/kr/pretty" "github.com/mtneug/pkg/ulid" cmap "github.com/streamrail/concurrent-map" "github.com/xtaci/smux" @@ -218,7 +217,6 @@ func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth f["stage"] = "checking domain" ln.Log(ctx, f) - pretty.Println(s.cfg.Storage) routeUser, err := s.cfg.Storage.HasRoute(auth.Domain) if err != nil { ln.Error(ctx, err, f) From adf2121326840d1f62256144b67523a45e0261df Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 00:19:56 -0700 Subject: [PATCH 16/24] internal/server: fixup logging of tcp/kcp servers listening --- internal/server/server.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/internal/server/server.go b/internal/server/server.go index 31a4c1e..4a8fb02 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -55,11 +55,6 @@ func (s *Server) listenTCP(ctx context.Context, addr string, tcfg *tls.Config) { panic(err) } - ln.Log(ctx, ln.F{ - "action": "tcp+tls listening", - "addr": l.Addr(), - }) - ln.Log(ctx, ln.Action("tcp+tls listening"), ln.F{"addr": l.Addr()}) for { @@ -84,10 +79,7 @@ func (s *Server) listenKCP(ctx context.Context, addr string, tcfg *tls.Config) { panic(err) } - ln.Log(ctx, ln.F{ - "action": "kcp+tls listening", - "addr": l.Addr(), - }) + ln.Log(ctx, ln.Action("kcp+tls listening"), ln.F{"addr": l.Addr()}) for { conn, err := l.Accept() From 2a06070becfa89b77171297207b04b91ff3587b5 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 00:26:43 -0700 Subject: [PATCH 17/24] tun2: fix TestBackendRouting --- internal/tun2/server.go | 5 +---- internal/tun2/server_test.go | 7 +++++-- internal/tun2/storage_test.go | 2 +- mage.go | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/tun2/server.go b/internal/tun2/server.go index c131d3c..810c1d0 100644 --- a/internal/tun2/server.go +++ b/internal/tun2/server.go @@ -214,8 +214,8 @@ func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth return "", nil, err } + f["auth_domain"] = auth.Domain f["stage"] = "checking domain" - ln.Log(ctx, f) routeUser, err := s.cfg.Storage.HasRoute(auth.Domain) if err != nil { @@ -225,7 +225,6 @@ func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth f["route_user"] = routeUser f["stage"] = "checking token" - ln.Log(ctx, f) tokenUser, scopes, err := s.cfg.Storage.HasToken(auth.Token) if err != nil { @@ -235,7 +234,6 @@ func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth f["token_user"] = tokenUser f["stage"] = "checking token scopes" - ln.Log(ctx, f) ok := false for _, sc := range scopes { @@ -251,7 +249,6 @@ func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth } f["stage"] = "user verification" - ln.Log(ctx, f) if routeUser != tokenUser { ln.Error(ctx, ErrAuthMismatch, f) diff --git a/internal/tun2/server_test.go b/internal/tun2/server_test.go index 0ead668..f84d300 100644 --- a/internal/tun2/server_test.go +++ b/internal/tun2/server_test.go @@ -199,6 +199,9 @@ func TestBackendRouting(t *testing.T) { ServerAddr: l.Addr().String(), Token: token, BackendURL: ts.URL, + Domain: domain, + + forceTCPClear: true, } c, err := NewClient(cc) @@ -206,9 +209,9 @@ func TestBackendRouting(t *testing.T) { t.Fatal(err) } - go c.Connect(ctx) // + go c.Connect(ctx) // TODO: fix the client library so this ends up actually getting cleaned up - time.Sleep(5 * time.Second) + time.Sleep(time.Second) req, err := http.NewRequest("GET", "http://cetacean.club/", nil) if err != nil { diff --git a/internal/tun2/storage_test.go b/internal/tun2/storage_test.go index f7e068f..ed50fb8 100644 --- a/internal/tun2/storage_test.go +++ b/internal/tun2/storage_test.go @@ -57,7 +57,7 @@ func (ms *mockStorage) HasRoute(domain string) (string, error) { user, ok := ms.domains[domain] if !ok { - return "", nil + return "", errors.New("no such route") } return user, nil diff --git a/mage.go b/mage.go index 6c50aac..cdddede 100644 --- a/mage.go +++ b/mage.go @@ -199,5 +199,5 @@ func Test() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - shouldWork(ctx, nil, wd, "go", "test", "-race", "-v", "./...") + shouldWork(ctx, nil, wd, "go", "test", "-v", "./...") } From 0b46f035c6b30e555a167fed59317358627b624d Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 01:00:42 -0700 Subject: [PATCH 18/24] tun2: cleanup server when done with it --- internal/tun2/server_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/tun2/server_test.go b/internal/tun2/server_test.go index f84d300..4ce5623 100644 --- a/internal/tun2/server_test.go +++ b/internal/tun2/server_test.go @@ -76,6 +76,7 @@ func TestBackendAuthV1(t *testing.T) { if err != nil { t.Fatal(err) } + defer s.Close() st.AddRoute(domain, user) st.AddToken(token, user, []string{"connect"}) @@ -167,6 +168,7 @@ func TestBackendRouting(t *testing.T) { if err != nil { t.Fatal(err) } + defer s.Close() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { From 73f278aed2dc517fd015c8f361c6dacf334eb815 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 10:17:07 -0700 Subject: [PATCH 19/24] tun2: make Server benchmarks, disabled for now --- internal/tun2/server.go | 18 ++++++-- internal/tun2/server_test.go | 90 ++++++++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 5 deletions(-) diff --git a/internal/tun2/server.go b/internal/tun2/server.go index 810c1d0..587d796 100644 --- a/internal/tun2/server.go +++ b/internal/tun2/server.go @@ -135,7 +135,7 @@ func (s *Server) Wait() { // Listen passes this Server a given net.Listener to accept backend connections. func (s *Server) Listen(l net.Listener, isKCP bool) { - ctx := context.Background() + ctx := s.ctx f := ln.F{ "listener_addr": l.Addr(), @@ -143,6 +143,12 @@ func (s *Server) Listen(l net.Listener, isKCP bool) { } for { + select { + case <-ctx.Done(): + return + default: + } + conn, err := l.Accept() if err != nil { ln.Error(ctx, err, f, ln.Action("accept connection")) @@ -326,12 +332,14 @@ func (s *Server) HandleConn(c net.Conn, isKCP bool) { if err != nil { connection.cancel() } - case <-s.ctx.Done(): - s.removeConn(ctx, connection) - connection.Close() + // case <-s.ctx.Done(): + // ln.Log(ctx, connection, ln.Action("server context finished")) + // s.removeConn(ctx, connection) + // connection.Close() - return + // return case <-ctx.Done(): + ln.Log(ctx, connection, ln.Action("client context finished")) s.removeConn(ctx, connection) connection.Close() diff --git a/internal/tun2/server_test.go b/internal/tun2/server_test.go index 4ce5623..00a3464 100644 --- a/internal/tun2/server_test.go +++ b/internal/tun2/server_test.go @@ -232,3 +232,93 @@ func TestBackendRouting(t *testing.T) { }) } } + +func setupTestServer() (*Server, *mockStorage, net.Listener, error) { + st := MockStorage() + + st.AddRoute(domain, user) + st.AddToken(token, user, []string{"connect"}) + + s, err := NewServer(&ServerConfig{ + Storage: st, + }) + if err != nil { + return nil, nil, nil, err + } + defer s.Close() + + l, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, nil, nil, err + } + + go s.Listen(l, false) + + return s, st, l, nil +} + +func BenchmarkHTTP200(b *testing.B) { + b.Skip("this benchmark doesn't work yet") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + s, _, l, err := setupTestServer() + if err != nil { + b.Fatal(err) + } + defer s.Close() + defer l.Close() + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + defer ts.Close() + + cc := &ClientConfig{ + ConnType: "tcp", + ServerAddr: l.Addr().String(), + Token: token, + BackendURL: ts.URL, + Domain: domain, + + forceTCPClear: true, + } + + c, err := NewClient(cc) + if err != nil { + b.Fatal(err) + } + + go c.Connect(ctx) // TODO: fix the client library so this ends up actually getting cleaned up + + for { + r := s.GetBackendsForDomain(domain) + if len(r) == 0 { + time.Sleep(125 * time.Millisecond) + continue + } + + break + } + + req, err := http.NewRequest("GET", "http://cetacean.club/", nil) + if err != nil { + b.Fatal(err) + } + + _, err = s.RoundTrip(req) + if err != nil { + b.Fatalf("got error on initial request exchange: %v", err) + } + + for n := 0; n < b.N; n++ { + resp, err := s.RoundTrip(req) + if err != nil { + b.Fatalf("got error on %d: %v", n, err) + } + + if resp.StatusCode != http.StatusOK { + b.Fail() + b.Logf("got %d instead of 200", resp.StatusCode) + } + } +} From c3b786b992971ba9f22a9d7d6b3f00d3e86b36d2 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 13:52:46 -0700 Subject: [PATCH 20/24] tun2: put content length in response --- internal/tun2/connection.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/tun2/connection.go b/internal/tun2/connection.go index 9db2195..b68cfe4 100644 --- a/internal/tun2/connection.go +++ b/internal/tun2/connection.go @@ -2,11 +2,10 @@ package tun2 import ( "bufio" - "bytes" "context" - "io/ioutil" "net" "net/http" + "strconv" "time" "github.com/Xe/ln" @@ -127,7 +126,6 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { if err != nil { return nil, errors.Wrap(err, ErrCantOpenSessionStream.Error()) } - defer stream.Close() err = req.Write(stream) if err != nil { @@ -142,13 +140,13 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + cl := resp.Header.Get("Content-Length") + asInt, err := strconv.Atoi(cl) if err != nil { - return nil, errors.Wrap(err, "can't read response body") + return nil, err } - resp.Body = ioutil.NopCloser(bytes.NewBuffer(body)) - resp.ContentLength = int64(len(body)) + resp.ContentLength = int64(asInt) return resp, nil } From 30f7f4636ec8312b8f14fa6c7de9257614fc2484 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 15:46:17 -0700 Subject: [PATCH 21/24] tun2: close http streams after 30 minutes --- internal/tun2/connection.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/tun2/connection.go b/internal/tun2/connection.go index b68cfe4..40f7c4d 100644 --- a/internal/tun2/connection.go +++ b/internal/tun2/connection.go @@ -127,6 +127,12 @@ func (c *Connection) RoundTrip(req *http.Request) (*http.Response, error) { return nil, errors.Wrap(err, ErrCantOpenSessionStream.Error()) } + go func() { + time.Sleep(30 * time.Minute) + + stream.Close() + }() + err = req.Write(stream) if err != nil { return nil, errors.Wrap(err, ErrCantWriteRequest.Error()) From 47ba6d6abef6b37dcefa44d0091c2687ee5bc7e1 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Wed, 4 Oct 2017 01:16:59 -0700 Subject: [PATCH 22/24] tun2: remove more debugging logging code --- internal/tun2/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/tun2/server.go b/internal/tun2/server.go index 587d796..54d5dbf 100644 --- a/internal/tun2/server.go +++ b/internal/tun2/server.go @@ -210,7 +210,6 @@ func (s *Server) backendAuthv1(ctx context.Context, st io.Reader) (string, *Auth } f["stage"] = "json decoding" - ln.Log(ctx, f) d := json.NewDecoder(st) var auth Auth From e1e255b138999bd7f509b38bae9e4d723bfecd4a Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Thu, 5 Oct 2017 13:15:24 -0700 Subject: [PATCH 23/24] plugins/autohttpagent: retry everything --- plugins/autohttpagent/main.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/autohttpagent/main.go b/plugins/autohttpagent/main.go index d2d8de2..c8cf89d 100644 --- a/plugins/autohttpagent/main.go +++ b/plugins/autohttpagent/main.go @@ -32,7 +32,7 @@ func mustEnv(key string, def string) string { return val } -func doHttpAgent() { +func doHTTPAgent() { go func() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -47,17 +47,17 @@ func doHttpAgent() { } client, _ := tun2.NewClient(cfg) - err := client.Connect() + err := client.Connect(ctx) if err != nil { ln.Error(ctx, err, ln.Action("client connection error, restarting")) time.Sleep(5 * time.Second) - doHttpAgent() + doHTTPAgent() } }() } func init() { - doHttpAgent() + doHTTPAgent() } From 0a5ff8dfaf139de9ac1ac0cdaa7f797d1bcb05c1 Mon Sep 17 00:00:00 2001 From: Christine Dodrill Date: Fri, 6 Oct 2017 10:54:09 -0700 Subject: [PATCH 24/24] internal/server: clean up NewServer --- internal/server/server.go | 18 ++++++++---------- internal/server/server_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 internal/server/server_test.go diff --git a/internal/server/server.go b/internal/server/server.go index 4a8fb02..313e9c6 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -13,6 +13,7 @@ import ( proto "git.xeserv.us/xena/route/proto" "github.com/Xe/ln" "github.com/mtneug/pkg/ulid" + "github.com/oxtoacart/bpool" kcp "github.com/xtaci/kcp-go" "golang.org/x/crypto/acme/autocert" "golang.org/x/net/context" @@ -136,18 +137,15 @@ func New(cfg Config) (*Server, error) { Manager: m, } - go s.listenKCP(context.Background(), cfg.BackendKCPAddr, &tls.Config{ + tc := &tls.Config{ GetCertificate: m.GetCertificate, - }) + } - go s.listenTCP(context.Background(), cfg.BackendTCPAddr, &tls.Config{ - GetCertificate: m.GetCertificate, - }) + go s.listenKCP(context.Background(), cfg.BackendKCPAddr, tc) + go s.listenTCP(context.Background(), cfg.BackendTCPAddr, tc) - gs := grpc.NewServer(grpc.Creds(credentials.NewTLS(&tls.Config{ - GetCertificate: m.GetCertificate, - InsecureSkipVerify: true, - }))) + // gRPC setup + gs := grpc.NewServer(grpc.Creds(credentials.NewTLS(tc))) proto.RegisterBackendsServer(gs, &Backend{Server: s}) proto.RegisterRoutesServer(gs, &Route{Server: s}) @@ -194,7 +192,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { Director: s.Director, Transport: s.ts, FlushInterval: 1 * time.Second, - //BufferPool: bpool.NewBytePool(256, 4096), + BufferPool: bpool.NewBytePool(256, 4096), } rp.ServeHTTP(w, r) diff --git a/internal/server/server_test.go b/internal/server/server_test.go new file mode 100644 index 0000000..506fdb3 --- /dev/null +++ b/internal/server/server_test.go @@ -0,0 +1,29 @@ +package server + +import ( + "net/http" + "testing" +) + +func TestDirector(t *testing.T) { + s := &Server{} + + req, err := http.NewRequest("GET", "https://cetacean.club/", nil) + if err != nil { + t.Fatal(err) + } + + req.Header.Add("X-Forwarded-For", "Rick-James") + req.Header.Add("X-Client-Ip", "56.32.51.84") + + s.Director(req) + + for _, header := range []string{"X-Forwarded-For", "X-Client-Ip"} { + t.Run(header, func(t *testing.T) { + val := req.Header.Get(header) + if val != "" { + t.Fatalf("expected header %q to have no value, got: %v", header, val) + } + }) + } +}