git » gofer » commit b06f6e4

raw: Implement a raw proxy

author Alberto Bertogli
2017-08-07 20:39:22 UTC
committer Alberto Bertogli
2017-08-07 20:57:33 UTC
parent 26144fa86eac9328459025270759b9ba353d5a8a

raw: Implement a raw proxy

This patch introduces a protocol-agnostic "raw" proxy.
It can listen on a TLS socket, and can also connect to a TLS backend;
these two are independent.

config/config.go +9 -1
config/config_test.go +14 -0
gofer.conf.example +17 -2
gofer.go +4 -0
proxy/{http_test.go => proxy_test.go} +30 -13
proxy/raw.go +67 -0

diff --git a/config/config.go b/config/config.go
index 6fee49e..3fc3c8d 100644
--- a/config/config.go
+++ b/config/config.go
@@ -14,8 +14,9 @@ type Config struct {
 
 	HTTP  []*HTTP
 	HTTPS []*HTTPS
+	Raw   []Raw
 
-	// Map of name -> routes.
+	// Map of name -> routes for HTTP(S).
 	Routes map[string]RouteTable
 }
 
@@ -30,6 +31,13 @@ type HTTPS struct {
 	Certs string
 }
 
+type Raw struct {
+	Addr  string
+	Certs string
+	To    string
+	ToTLS bool `toml:"to_tls",omitempty`
+}
+
 type RouteTable map[string]string
 
 // mergeRoutes merges the table src into dst, by adding the entries in src
diff --git a/config/config_test.go b/config/config_test.go
index d414e11..f9bb769 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -10,6 +10,12 @@ func TestSimple(t *testing.T) {
 	const contents = `
 control_addr = "127.0.0.1:9081"
 
+[[raw]]
+addr = ":995"
+certs = "/etc/letsencrypt/live/"
+to = "blerg.com:1995"
+to_tls = true
+
 [[http]]
 addr = ":http"
 base_routes = "default"
@@ -33,6 +39,14 @@ base_routes = "default"
 
 	expected := Config{
 		ControlAddr: "127.0.0.1:9081",
+		Raw: []Raw{
+			Raw{
+				Addr:  ":995",
+				Certs: "/etc/letsencrypt/live/",
+				To:    "blerg.com:1995",
+				ToTLS: true,
+			},
+		},
 		HTTP: []*HTTP{
 			&HTTP{
 				Addr:       ":http",
diff --git a/gofer.conf.example b/gofer.conf.example
index 6fa0d96..2c09f9b 100644
--- a/gofer.conf.example
+++ b/gofer.conf.example
@@ -10,13 +10,28 @@ certs = "/etc/letsencrypt/live/"
 # Take the routes from the definition below as a baseline.
 base_routes = "default"
 
-# Extend the base routes.
-routes = { "/local/" = "http://localhost:99/" }
+  # Extend the base routes.
+  [https.routes]
+  "/local/" = "http://localhost:99/"
+
 
 [[http]]
 addr = ":http"
 base_routes = "default"
 
+
 [routes.default]
 "/" = "http://localhost:8080/"
 
+
+[[raw]]
+addr = ":995"
+
+# If this is present, we will listen on a TLS socket; otherwise it will be a
+# plain socket.
+certs = "/etc/letsencrypt/live/"
+
+# Where to connect to. If to_tls is true, then we will do TLS against the
+# backend.
+to = "example.com:1995"
+to_tls = true
diff --git a/gofer.go b/gofer.go
index 46861a8..f625877 100644
--- a/gofer.go
+++ b/gofer.go
@@ -34,6 +34,10 @@ func main() {
 		go proxy.HTTP(*http)
 	}
 
+	for _, raw := range conf.Raw {
+		go proxy.Raw(raw)
+	}
+
 	// Monitoring server.
 	if conf.ControlAddr != "" {
 		mux := http.NewServeMux()
diff --git a/proxy/http_test.go b/proxy/proxy_test.go
similarity index 75%
rename from proxy/http_test.go
rename to proxy/proxy_test.go
index 8df0d40..772b1fc 100644
--- a/proxy/http_test.go
+++ b/proxy/proxy_test.go
@@ -14,11 +14,6 @@ import (
 	"blitiri.com.ar/go/gofer/config"
 )
 
-const configTemplate = `
-[[http]]
-addr = "$FRONTEND_ADDR"
-routes = { "/be/" = "$BACKEND_URL" }
-`
 const backendResponse = "backend response\n"
 
 func TestSimple(t *testing.T) {
@@ -28,28 +23,50 @@ func TestSimple(t *testing.T) {
 		}))
 	defer backend.Close()
 
-	feAddr := getFreePort()
+	// We have two frontends: one raw and one http.
+	rawAddr := getFreePort()
+	httpAddr := getFreePort()
 
+	const configTemplate = `
+[[raw]]
+addr = "$RAW_ADDR"
+to = "$BACKEND_ADDR"
+
+[[http]]
+addr = "$HTTP_ADDR"
+routes = { "/be/" = "$BACKEND_URL" }
+`
 	configStr := strings.NewReplacer(
-		"$FRONTEND_ADDR", feAddr,
+		"$RAW_ADDR", rawAddr,
+		"$HTTP_ADDR", httpAddr,
 		"$BACKEND_URL", backend.URL,
+		"$BACKEND_ADDR", backend.Listener.Addr().String(),
 	).Replace(configTemplate)
 
 	conf, err := config.LoadString(configStr)
 	if err != nil {
 		log.Fatal(err)
 	}
+	t.Logf("conf.Raw[0]: %#v", conf.Raw[0])
 	t.Logf("conf.HTTP[0]: %#v", *conf.HTTP[0])
 
+	go Raw(conf.Raw[0])
 	go HTTP(*conf.HTTP[0])
 
-	waitForHTTPServer(feAddr)
+	waitForHTTPServer(httpAddr)
+	waitForHTTPServer(rawAddr)
+
+	// Test the raw proxy.
+	testGet(t, "http://"+rawAddr+"/be", 200)
+
+	// Test the HTTP proxy. Try a combination of URLs and error responses just
+	// to exercise a bit more of the path handling and error checking code.
+	testGet(t, "http://"+httpAddr+"/be", 200)
+	testGet(t, "http://"+httpAddr+"/be/", 200)
+	testGet(t, "http://"+httpAddr+"/be/2", 200)
+	testGet(t, "http://"+httpAddr+"/be/3", 200)
+	testGet(t, "http://"+httpAddr+"/x", 404)
 
-	testGet(t, "http://"+feAddr+"/be", 200)
-	testGet(t, "http://"+feAddr+"/be/", 200)
-	testGet(t, "http://"+feAddr+"/be/2", 200)
-	testGet(t, "http://"+feAddr+"/be/3", 200)
-	testGet(t, "http://"+feAddr+"/x", 404)
 }
 
 func testGet(t *testing.T, url string, expectedStatus int) {
diff --git a/proxy/raw.go b/proxy/raw.go
new file mode 100644
index 0000000..d57902b
--- /dev/null
+++ b/proxy/raw.go
@@ -0,0 +1,67 @@
+package proxy
+
+import (
+	"crypto/tls"
+	"net"
+
+	"blitiri.com.ar/go/gofer/config"
+	"blitiri.com.ar/go/gofer/util"
+)
+
+func Raw(conf config.Raw) {
+	var err error
+
+	var tlsConfig *tls.Config
+	if conf.Certs != "" {
+		tlsConfig, err = util.LoadCerts(conf.Certs)
+		if err != nil {
+			util.Log.Fatalf("error loading certs: %v", err)
+		}
+	}
+
+	var lis net.Listener
+	if tlsConfig != nil {
+		lis, err = tls.Listen("tcp", conf.Addr, tlsConfig)
+	} else {
+		lis, err = net.Listen("tcp", conf.Addr)
+	}
+	if err != nil {
+		util.Log.Fatalf("error listening: %v", err)
+	}
+
+	util.Log.Printf("Raw proxy on %q", conf.Addr)
+	for {
+		conn, err := lis.Accept()
+		if err != nil {
+			util.Log.Fatalf("%s error accepting: %v", conf.Addr, err)
+		}
+
+		go forward(conn, conf.To, conf.ToTLS)
+	}
+}
+
+func forward(src net.Conn, dstAddr string, dstTLS bool) {
+	defer src.Close()
+
+	var dst net.Conn
+	var err error
+	if dstTLS {
+		dst, err = tls.Dial("tcp", dstAddr, nil)
+	} else {
+		dst, err = net.Dial("tcp", dstAddr)
+	}
+
+	if err != nil {
+		util.Log.Printf("%s error dialing back: %v", src.LocalAddr(), err)
+		return
+	}
+	defer dst.Close()
+
+	util.Log.Printf("%s raw %s -> %s: open",
+		src.RemoteAddr(), src.LocalAddr(), dst.RemoteAddr())
+
+	util.BidirCopy(src, dst)
+
+	util.Log.Printf("%s raw %s -> %s: close",
+		src.RemoteAddr(), src.LocalAddr(), dst.RemoteAddr())
+}